Compare commits

...

404 Commits

Author SHA1 Message Date
Matthieu Gallien f15810ae0d
Merge pull request #6758 from nextcloud/ci/githubActionsForAppimage
produce Appimage packages from github actions
2024-05-16 15:39:54 +02:00
Matthieu Gallien df7527c310 produce Appimage packages from github actions
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-16 11:56:22 +02:00
Nextcloud bot 064e134569
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-05-16 02:34:05 +00:00
Claudio Cambra 22f2fe1218
Merge pull request #6706 from nextcloud/bugfix/account-details 2024-05-15 21:47:54 +02:00
Claudio Cambra 57ced98f98 Censor account details string in file provider logging
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-05-15 21:47:05 +02:00
Matthieu Gallien a06fe10f65
Merge pull request #6713 from nextcloud/ci/fixWindowsTests
some files just cannot sync on windows: get automated tests to work
2024-05-15 18:28:22 +02:00
Matthieu Gallien 9ed87bf2ad some files just cannot sync on windows: get automated tests to work
fix one automated test knowing the platform limitations of windows

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 17:48:58 +02:00
Matthieu Gallien d228ec445f
Merge pull request #6756 from nextcloud/ci/fixMacCi
skip tests currently broken on macOS: enable mandatory tests for macOS
2024-05-15 17:39:35 +02:00
Matthieu Gallien 3626e4c0c0 skip tests currently broken on macOS: enable mandatory tests for macOS
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 17:05:46 +02:00
Matthieu Gallien 36c2c618ec
Merge pull request #6715 from nextcloud/updateInstallRequirementsInDoc
update doc with install requirements fom Qt6 supported platforms
2024-05-15 17:01:59 +02:00
Matthieu Gallien 3342a13f44
update doc with install requirements for Qt6 supported platforms
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:49:53 +02:00
Camila Ayres 6a6138c6b4
Merge pull request #6725 from nextcloud/update/documentation
Extend 'How the "Edit locally" functionality works' text.
2024-05-15 15:42:33 +02:00
Camila Ayres 011dd6c4cb Extend 'How the "Edit locally" functionality works' text.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-05-15 15:42:23 +02:00
Matthieu Gallien 42ed8b454c
Merge pull request #6705 from nextcloud/ci/fixWindowsCi
use windows-2022 image to run our windows CI on github actions
2024-05-15 15:38:33 +02:00
Matthieu Gallien a3083f8be4 skip FolderWatcher on windows: really not a reliable test
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:11 +02:00
Matthieu Gallien e2ed718030 disable some unreliable tests for CfApi CI
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:11 +02:00
Matthieu Gallien 7c9f652c3e fix regression with VFS update metadata instruction
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:11 +02:00
Matthieu Gallien 91bea6f305 only compute checksum of a local file that is not a virtual one
should avoid triggering implicit hydration from within the desktop
client

triggering an implicit hydration on our own is strictly forbidden as
that is causing issues like deadlock and failed hydration attempts

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:11 +02:00
Matthieu Gallien a1efabc3b6 remove usage of memory sanitizers in drone tests
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:11 +02:00
Matthieu Gallien 098f4ef164 better automated tests log
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:11 +02:00
Matthieu Gallien 7cde0b16e1 catch std::filesystem exceptions in automated tests
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:10 +02:00
Matthieu Gallien 96d1fc0720 temporarily ignore failed tests on windows
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:10 +02:00
Claudio Cambra df93608477 Update macos-build-and-test.yml with latest Xcode
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-05-15 15:06:10 +02:00
Matthieu Gallien d4986e15f8 simplify macOS targets and try arm64 craft target
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:10 +02:00
Matthieu Gallien aa38a0180d use windows-2022 image to run our windows CI on github actions
also switch craft to target windows-msvc2022_64-cl

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 15:06:10 +02:00
Matthieu Gallien 1b9fbb03bb
Merge pull request #6751 from nextcloud/bugfix/fixWrongMemoryAccessInExcludeFiles
avoid accessing a temp QString via QStringView after it is deleted
2024-05-15 14:34:57 +02:00
Matthieu Gallien ba00c50022 avoid accessing a temp QString via QStringView after it is deleted
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-15 14:34:46 +02:00
Claudio Cambra 62661b7a0b
Merge pull request #6753 from nextcloud/bugfix/mdm-postinstall-mac
Do not open client on install as this breaks MDM deployments
2024-05-15 14:29:35 +02:00
Claudio Cambra 9eb8645b95 Do not open client on install as this breaks MDM deployments
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-05-15 14:28:49 +02:00
Nextcloud bot 5e70aa3634
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-05-15 02:31:38 +00:00
Claudio Cambra c8d756ee65
Merge pull request #6736 from nextcloud/update/docs-qt6
Update the documentation on how to build the client
2024-05-14 12:42:05 +02:00
Camila Ayres 71098127fc Improve text about %PATH% and KDE Craft.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-05-14 10:28:17 +02:00
Camila Ayres 88ad7e3b02 Remove duplicated instructions.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-05-14 10:28:17 +02:00
Camila Ayres 4fe7cdd5b0 Fix documentation style.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-05-14 10:28:17 +02:00
Camila Ayres 63fdce3fbe Update instructions to build the client on mac OS.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-05-14 10:28:17 +02:00
Camila Ayres 6d3335bd60 Update instructions to build on Windows.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-05-14 10:28:17 +02:00
Camila Ayres 7d575d9bf0 Add documentation about how to compile the client on Windows with Qt6.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-05-14 10:28:17 +02:00
Claudio Cambra 3dd3082a66
Merge pull request #6733 from nextcloud/dependabot/github_actions/skjnldsv/block-fixup-merge-action-2
Bump skjnldsv/block-fixup-merge-action from 1 to 2
2024-05-14 10:27:56 +02:00
dependabot[bot] a6d48ef5db Bump skjnldsv/block-fixup-merge-action from 1 to 2
Bumps [skjnldsv/block-fixup-merge-action](https://github.com/skjnldsv/block-fixup-merge-action) from 1 to 2.
- [Release notes](https://github.com/skjnldsv/block-fixup-merge-action/releases)
- [Commits](42d26e1b53...c138ea99e4)

---
updated-dependencies:
- dependency-name: skjnldsv/block-fixup-merge-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 09:56:54 +02:00
Nextcloud bot 52e6b8216f
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-05-14 02:34:12 +00:00
Claudio Cambra 390d6a58a4
Merge pull request #6743 from nextcloud/bugfix/mac-build-qtsix
Fix macOS build on master
2024-05-13 11:10:05 +02:00
Claudio Cambra 4bd0974696 Remove use of setMargin on QVBoxLayout, not present in Qt6
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-05-13 11:07:33 +02:00
Claudio Cambra afbd2ebc02 Remove all use of MacExtras
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-05-13 11:07:33 +02:00
allexzander 7a49312ddd
Merge pull request #6742 from nextcloud/bugfix/lockownertype-setreadonly-basedoncaps
Bugfix/lockownertype setreadonly basedoncaps
2024-05-12 12:53:56 +02:00
Nextcloud bot 7bd2b8aeff
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-05-11 02:34:15 +00:00
alex-z 04c0125bd1 Fix incorrect date parsing.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-05-09 16:21:46 +02:00
alex-z c7591f6332 Bugfix. Files lock. Check lock owner type for setting readonly based on server capabilities (NC27/28 compatibility issue).
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-05-09 16:21:46 +02:00
Nextcloud bot cf888027df
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-05-08 02:47:33 +00:00
Nextcloud bot 0d7ab96330
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-05-03 02:40:59 +00:00
Matthieu Gallien 73c2f17c59
Merge pull request #6718 from nextcloud/ci/improveDevModeOnWindows
improve logs when build with NEXTCLOUD_DEV enabled
2024-05-02 18:17:06 +02:00
Matthieu Gallien d0b4af6ccc improve logs when build with NEXTCLOUD_DEV enabled
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-05-02 16:34:51 +02:00
Nextcloud bot 75d0e9aebe
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-05-02 02:40:54 +00:00
Nextcloud bot 593d133a20
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-05-01 02:42:24 +00:00
Nextcloud bot 8cbb7c3cd3
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-30 02:41:22 +00:00
Matthieu Gallien 5ee1afe3dd
Merge pull request #6710 from nextcloud/ci/improveBuildAppimageScript
Ci/improve build appimage script
2024-04-29 11:14:42 +02:00
Matthieu Gallien 64d54a17b6
as far as I can tell icon name is always Nextcloud.png
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-29 11:13:56 +02:00
Matthieu Gallien b58723fe6f
buildAppimage script gets configurable paths via env variables
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-29 10:02:50 +02:00
Matthieu Gallien 2ba242527c
makes Qt path and openssl path depend on environment variable
provide default value that may work outside our docker build images

will get sensible value when run inside our official docker build images

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-29 09:52:54 +02:00
Nextcloud bot f4d9fbc2e7
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-29 02:40:52 +00:00
Claudio Cambra 6a6d92d89b
Merge pull request #6602 from nextcloud/bugfix/clarifyInvalidFileNamesWarning
invalid item name warning: use file or folder when appropriate
2024-04-28 17:01:38 +08:00
Matthieu Gallien 7262d11f47 invalid item name warning: use file or folder when appropriate
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-28 16:57:22 +08:00
Claudio Cambra 95c7debfb5
Merge pull request #6703 from nextcloud/rakekniven-patch-1
chore(i18n): Improve grammar
2024-04-28 16:50:41 +08:00
rakekniven 9ec5712b21 chore(i18n): Improve grammar
Fixes #6408 

Reported at Transifex.

Signed-off-by: rakekniven <2069590+rakekniven@users.noreply.github.com>
2024-04-28 16:48:08 +08:00
Nextcloud bot 06c4ea8e25
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-27 02:50:59 +00:00
Matthieu Gallien 810d8f15ee
Merge pull request #4584 from nextcloud/feature/qt6
Migrate to Qt 6
2024-04-26 09:05:38 +02:00
Matthieu Gallien 3770eec050 windows needs an explicit QML import path to be set
on windows qml modules are not default loaded from the install folder of the app

set it such that qml modules are imported from teh installed qml folder

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 07c2554eb3 AppImage build tool for Qt needs to know our qml files
gives the path to qml files such that needed Qt qml modules are deployed
correctly inside the AppImage package

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 347285b5b3 fix build issue after rebase
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 5bed41a670 use the correct Qt6 build in newer CI images
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 794db304f9 fix compilation of AppImage packages
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 2d5753c17d make it easy to find out that this branch is Qt6 based
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien a357570633 fix compilation after rebase
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien bebb8e1954 fix compilation issues on windows
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 20db6b6d86 fully qualify types for use with Qt metaobject system
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 1533670e77 add NextcloudSslCertificate to wrap QSslCertificate in QHash
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien a05ac621bf do some header includes clean-up as recommended by compiler
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 9b9ff4f471 add needed namespace for declarations read by Qt metaobject system
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 3099628770 header changes needed for clang/vs2022 compilers
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 82a0c1d054 NetrcParser tests are known to be broken
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 7d132029a2 use new qt 6.6.3 ci images
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 060181f83d fix some of the qml issues
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien aae9e84438 fix compilation issues after rebase
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
tobiasKaminsky 8ad2a82ea9 Signed drone.yml
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 4f178fa9be fix order of init in a constructor to avoid warnings
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 3ad7ac922a avoid warning by ignoring the return value of QtConcurrent::run
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 910d7f03e8 when loading translation catalog, do not ignore the return value
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 7432fb4980 adapt ci checks to Qt6
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 430d56e72e build image for Qt6
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien ced6d3274c build appimage
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 332f069491 use the new CI images with Qt6
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien b7bba50672 let appimage build script work with Qt6 based version
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 44f6d514ff only try to include qt6-keychain header and not the qt5 version
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 032af80d4d fix computation of timeout for jobs to have proper bounds (min < max)
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 09e60744c2 e proper data type in data for activity model automated test data
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien fa766c02ac properly use QStringView to avoid copies
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 670b2ce42f fix automated tests missing toString() convert from QVariant
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 274d866c19 fix failing automated test that erases invalid iterator
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien ced85ac287 fix automated tests with network requests
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien e3456847d8 last step
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 70931fb0af Remove commented out broken quick compiler check in Qt6
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 5d765dd017 Fix user status selector
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 3765df627b Fix test compilation
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 3597766fb0 Add separators to SyncStatus and UnifiedSearchInputContainer to stop the scrollviews looking broken
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 52758a00b8 Fix QML coloring issues
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 5087d5142a Fix all broken QML imports
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 7e62368eb2 Fix QDateTime string formatting
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra c31e65c111 Remove crashing QRandomGenerator seed call
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra a0e90cf56b Remove use of QCoreApplication AA attributes
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra a8e7e340aa Replace use of staticQtMetaObject with staticMetaObject
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra aaea45110f Replace removed progress bar option orientation with state flag
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 7a17a51a25 Use QEnterEvent for new enterEvent parameters
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 7e1448bcf2 Remove use of qRegisterMetatypeStreamOperators
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 84414ce5dc Replace deleted '+' operator for flags with '|' operator
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 6210490109 Replace now invalid '+' operator with '|' operator for QKeySequence
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 1a2db488ab Add missing QActionGroup include
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 7954695783 Add missing QStandardPaths include
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra ac1206a0c1 Remove conflicting alias to QStringList
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 366f5f0303 Fix bad conversion to bool of shared pointer
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 6a497cf21c Fix QTextCodec related build issues
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 4cad9ebdac Fix type of decpoint
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 9932956686 Remove qtokenizer in favour of Qt6 QStringTokenizer
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 29b0d2b8ad Remove use of QNetworkConfiguration in Qt6
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra 79a150baf4 Fix qtkeychain imports with Qt6
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Claudio Cambra fe7c00a7bf Fix macOS-specific CMake things with Qt6
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 4566400ee6 streamline find_package calls to really find Qt6
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien aa76de9b68 allow detection of qt5 or qt6
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien d3442d137a disable qt apis deprecated before qt 5.12, enable warnings
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 1f0279e1c1 remove usage of QStringRef due to it being missing in Qt6
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien b63c88e492 add missing QStringLiteral
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien ee245f26c8 replace deprecated QWebEngineProfile::setRequestInterceptor
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 249e0eb87d port away from QWidget related margines APIs
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 20ee506b71 port away from deprecated API from QFontMetricsF
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien d035c26be5 replace qrand/qsrand
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien b712108229 add missing Qt:: namespace when using Qt::endl
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 80b25d36fa port away from QStringList::toSet
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Matthieu Gallien 21464063b6 port away from QDesktopServices::storageLocation
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-26 09:05:12 +02:00
Nextcloud bot aa175036e9
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-26 02:44:27 +00:00
allexzander 7e801d6c9a
Merge pull request #6691 from nextcloud/bugfix/slow-sync-with-tray-open
Bugfix/slow sync with tray open
2024-04-25 10:18:13 +02:00
Matthieu Gallien 59fc619c35
Merge pull request #6696 from nextcloud/bugfix/conflictdialog-multiple-darkmode
Bugfix. Conflict dialog for multiple files. Fix checkbox border colors for dark mode on Windows.
2024-04-25 10:14:21 +02:00
alex-z 2ea6d3fd38 Bugfix. Conflict dialog for multiple files. Fix checkbox border colors for dark mode on Windows.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-25 10:14:11 +02:00
Nextcloud bot 98e1d71f70
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-25 02:42:00 +00:00
alex-z 9fac497e6e Hotfix. Slow download speed while tray is open. Do not run rotation animation in syncstatus in tray.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-24 20:28:24 +02:00
alex-z fc31ac4a1f Just for test. Disable status update to avoid havin a sync status animation in tray.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-24 20:28:24 +02:00
Matthieu Gallien caa4d8943c
Merge pull request #6660 from nextcloud/bugfix/fileslock_incorrect_readonly
Bugfix. Files lock. Fix incorrect readonly state.
2024-04-24 15:17:39 +02:00
alex-z 47a605c654 Bugfix. Files lock. Fix incorrect readonly state.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-24 13:28:22 +02:00
Nextcloud bot e18ab96882
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-24 02:41:27 +00:00
Camila Ayres d415b08101
Revert "Bump version to 3.13.0."
This reverts commit 9f968f6821.
2024-04-23 20:37:38 +02:00
Camila Ayres 9f968f6821
Bump version to 3.13.0.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-04-23 20:26:09 +02:00
Claudio Cambra ef08c5eb4e Only show successful debug archive creation dialog if it has indeed been successful
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:16:53 +02:00
Claudio Cambra ef2423da53 Prevent crash on creating debug archive in non-writeable location
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:16:53 +02:00
Claudio Cambra 2323b843f0 Give debug archive save location dialog a default location
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:16:53 +02:00
Claudio Cambra 64b7282bd4 Re-run update sync paused state slot when folder list has changed
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:15:13 +02:00
Claudio Cambra d3aa7f8f51 Make sure to emit syncIsPausedChanged in syncIsPaused setter
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:15:13 +02:00
Claudio Cambra ccf6b5abe1 Make sure to emit relevant signals and set sync is paused to true if relevant
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:15:13 +02:00
Claudio Cambra d497e265df Extract syncIsPaused initialisation into new updater slot
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:15:13 +02:00
Claudio Cambra 92f6de9ca9 Always correctly set values for all fields in File Provider sharing UI
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:12:56 +02:00
Claudio Cambra 1c84b832fe Fetch macOS VFS package in autoupdater if the client is using the file provider module
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:11:41 +02:00
Claudio Cambra 591d5eebd2 Prevent use of invalid characters for file provider domain names
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:11:11 +02:00
Claudio Cambra 8f6c19e029 Fix "false" error about bad applying of nodiscard to value type
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 19:10:40 +02:00
Matthieu Gallien 510b3edc3c
Merge pull request #6606 from nextcloud/bugfix/doNotImplicitlyHydrateFilesDuringSync
do not cause implicit hydration of virtual files during sync
2024-04-23 18:28:34 +02:00
Matthieu Gallien 2141ccdb7d do not cause implicit hydration of virtual files during sync
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-23 18:28:25 +02:00
Matthieu Gallien 5450552339
Merge pull request #6621 from nextcloud/bugfix/fixMsgVfsState
if a virtual file change but nothing changed: set it as in sync
2024-04-23 18:04:48 +02:00
Matthieu Gallien 1522d01d5b if a virtual file change but bothing changed: set it as in sync
some software (at least outlook native software) may fiddle with CfApi
placeholder metadta and set a .msg file to be out of sync state when
opening it

in that case, we will let the sync engine go over it and decide what to
do

it is then possible in that case that we would just put it back in "in
sync" state

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-23 18:04:35 +02:00
Claudio Cambra 03fe6494e4
Merge pull request #6670 from nextcloud/bugfix/dav-user-fileprovider
Use davUser instead of direct credentials user in file provider
2024-04-23 23:55:33 +08:00
Claudio Cambra 80afd8e737
Use davUser instead of direct credentials user in file provider
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-23 22:24:20 +08:00
Matthieu Gallien c0af76ca5f
Merge pull request #6663 from nextcloud/bugfix/editLocallyShouldWorkForAllMachineUsers
create registry keys needed for edit locally in local machine category
2024-04-23 14:49:03 +02:00
Matthieu Gallien 06c2fecbe1 create registry keys needed for edit locally in local machine category
creating the registry keys via installer for the current user is not
enough to have the feature enabled for all user accounts on a given
machine

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-23 13:48:14 +02:00
Matthieu Gallien 23772f18e0
Merge pull request #6622 from nextcloud/ci/endToEndTests
improving end-to-end tests
2024-04-23 13:44:43 +02:00
Matthieu Gallien cd924de9d3
improving end-to-end tests
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-23 12:21:10 +02:00
Matthieu Gallien dd8a16f9d6
improving end-to-end tests
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2024-04-23 12:21:09 +02:00
allexzander f40b8ae198
Merge pull request #6655 from nextcloud/bugfix/folder-conflict-disappear
Bugfix/folder conflict disappear
2024-04-23 12:18:25 +02:00
alex-z b52906a8a6 Fix tests failure. Refactoring.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-23 12:18:12 +02:00
alex-z f490989a1a Fix CI errors.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-23 12:18:12 +02:00
alex-z 9ae60258e1 Unit tests for diverse conflicts in one folder.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-23 12:18:12 +02:00
alex-z 57f6c7cda2 Also support nested folder scenarios.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-23 12:18:12 +02:00
alex-z d2bfb59d6a Bugfix. Folder invalid char conflict. Do not update parent folder record if it contains conflicted subfolders.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-23 12:18:12 +02:00
alex-z d240ed9d50 Bugfix. Folder case clash conflict. Do not update parent folder record if it contains conflicted subfolers. Also fix crash with local rename.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-23 12:18:12 +02:00
Nextcloud bot f4acb5099f
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-23 02:43:21 +00:00
Matthieu Gallien b7dd6ff748
Merge pull request #6596 from pyromaniac2k/master
Fix tests
2024-04-22 16:08:38 +02:00
Thomas Witt a5a31321f8 UserStatusDialog.cpp: Fix timezone
This test fails on the day before a timezone switch:
   Actual   (model.clearAtDisplayString()): "23 hours"
   Expected (tr("1 day"))                 : "1 day"

Setting the timezone to UTC remedies this problem.

Signed-off-by: Thomas Witt <pyromaniac@exherbo.org>
2024-04-22 09:47:49 +02:00
Thomas Witt d0097ce25c SyncConfilctsModel test: use FakeAccountState
Using the real account state needs an internet connection.

Additionally, `example.de` is a valid existing domain, which should
probably not be used in testing. Switching to `example.com` as this is
recommended here.

Signed-off-by: Thomas Witt <pyromaniac@exherbo.org>
2024-04-22 09:47:49 +02:00
Thomas Witt 8a9de185a9 pushnotification testutils: Bind only to LocalHost
LocalHost is enough to complete the tests, and it enables the test to be
run under networksandboxing.

Signed-off-by: Thomas Witt <pyromaniac@exherbo.org>
2024-04-22 09:47:49 +02:00
Nextcloud bot 1cb798b6c7
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-22 02:42:25 +00:00
Nextcloud bot 0aea31d877
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-20 02:48:12 +00:00
Nextcloud bot 0203b5423f
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-19 02:43:07 +00:00
Claudio Cambra fcb5380437
Merge pull request #6648 from nextcloud/bugfix/fp-sharing
Fix possible issues with item metadata acquisition required for macOS VFS file sharing
2024-04-18 13:13:03 +08:00
Claudio Cambra b80afca177
Wrap access of itemUrl in security scoping
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-18 04:56:31 +08:00
Claudio Cambra 59928a6c33
Explicitly set bundle name and identifiers from env vars in FileProviderUIExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-18 04:56:31 +08:00
Claudio Cambra 96f1ba656f
Unify FileProviderUIExt entitlements
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-18 02:26:49 +08:00
Claudio Cambra 19cf69ccd3
Make sure network error is shown in UI instead of generic error
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-18 02:07:48 +08:00
Claudio Cambra ac1b11708f
Improve logging across file provider sharing
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-18 01:58:38 +08:00
Claudio Cambra a899fe19fa
Merge pull request #6632 from nextcloud/dependabot/github_actions/cpp-linter/cpp-linter-action-2.11.0
Build(deps): Bump cpp-linter/cpp-linter-action from 2.10.2 to 2.11.0
2024-04-17 17:22:26 +08:00
dependabot[bot] 75020c03ce Build(deps): Bump cpp-linter/cpp-linter-action from 2.10.2 to 2.11.0
Bumps [cpp-linter/cpp-linter-action](https://github.com/cpp-linter/cpp-linter-action) from 2.10.2 to 2.11.0.
- [Release notes](https://github.com/cpp-linter/cpp-linter-action/releases)
- [Commits](https://github.com/cpp-linter/cpp-linter-action/compare/v2.10.2...v2.11.0)

---
updated-dependencies:
- dependency-name: cpp-linter/cpp-linter-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-17 17:06:43 +08:00
Claudio Cambra aeca31af97
Merge pull request #6642 from nextcloud/bugfix/file-provider-testing
Make use of NextcloudFileProviderKit in File Provider Module
2024-04-17 16:21:16 +08:00
Claudio Cambra f0f995c260
Pin NextcloudFileProviderKit version to 0.9.0 (up to next major version)
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:18:17 +08:00
Claudio Cambra dff6428a75
Remove now-unneeded Realm dependency from FileProviderExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:17:42 +08:00
Claudio Cambra 77f9096538
Set changeobserver as delegate for nkcommon
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:05 +08:00
Claudio Cambra 5977a7c92d
Remove client-side push notification handling for file provider extension in favour of simply using NCFPK remote change observer
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:05 +08:00
Claudio Cambra 68370ade88
Add change observer from NCFPK
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:05 +08:00
Claudio Cambra 6a64248ff6
Always signal enumerator after errors to try and recover from what the error might have been
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:04 +08:00
Claudio Cambra 33e2c084a5
Provide NCFPK enumerator with extension domain
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:04 +08:00
Claudio Cambra 7664509e22
Correctly set up NCKit instance with account string
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:04 +08:00
Claudio Cambra 9e7ce1640d
Remove unused components in FileProviderExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:04 +08:00
Claudio Cambra c082c446c1
Fix passing of wrong item into item.modify
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:04 +08:00
Claudio Cambra dab28f20f4
Improve logging in delete item procedure of FPExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:04 +08:00
Claudio Cambra 485b07a805
Use NCFPK item modify method
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:03 +08:00
Claudio Cambra c9a131736a
Use NCFPK Item.create in createItem
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:03 +08:00
Claudio Cambra 2373cd4dde
Use fetchContents from NCFPK item
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:03 +08:00
Claudio Cambra 3ec18ba1a6
Simplify FileProviderExtension's deleteItem method by leveraging Item's delete method
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:03 +08:00
Claudio Cambra 788fd7f363
Greatly simplify item method of FIleProviderExtension by using item storedItem method
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:03 +08:00
Claudio Cambra 6200cab957
Use new convenience method to get Item for root container from NCFPK
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:03 +08:00
Claudio Cambra d74d23cedb
Use thumbnail fetching procedure from NCFPK
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:02 +08:00
Claudio Cambra 1f78b9f685
Fix build of ShareTableViewDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:02 +08:00
Claudio Cambra e8d1afa3df
Adapt to new NextcloudFileProviderKit item nomenclature
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:02 +08:00
Claudio Cambra 913d724254
Make use of NextcloudFileProviderKit materialisedenumerationobserver
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:02 +08:00
Claudio Cambra 2c0688f82b
Use FileProviderItem from NextcloudFileProviderKit
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:02 +08:00
Claudio Cambra 5a774756b6
Use Enumerator in NextcloudFileProviderKit
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:02 +08:00
Claudio Cambra b9483f0c55
Remove use of all code now available in NextcloudFileProviderKit
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:01 +08:00
Claudio Cambra e7616e0e54
Add NextcloudFileProviderKit dependency
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:01 +08:00
Claudio Cambra 3c3e3aa353
Remove FileProviderExtTests
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:01 +08:00
Claudio Cambra 3066f58673
Allow NextcloudFilesDatabaseManager to take a specific realmconfig in constructor
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:01 +08:00
Claudio Cambra 2caa43a76d
Database manager does not need to be an NSObject
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:01 +08:00
Claudio Cambra af9a271662
Add test target for file provider testing
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:15:00 +08:00
Claudio Cambra 3afa861f91
Merge pull request #6614 from nextcloud/feature/file-provider-sharing
File sharing for macOS VFS (File Provider Module)
2024-04-17 16:12:56 +08:00
Claudio Cambra d066536de0 Set file provider extension target as a dependency of file provider ui extension target in CMake
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 206d7cf3f4 Allow SuggestionsTextFieldKit to use up to next major version
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 5e80827c1f Upgrade NextcloudCapabilitiesKit version
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 1beb04371c Add a "no shares" label if there are no shares available in share view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 3bdb1ca1cb Update description label in share view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 5070c370a6 Display shareWith in nkshare displaystring extension
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra beedbbb471 Pin NextcloudCapabilitiesKit version
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra cfda22c107 Pin NextcloudKit version
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 1285b02770 Set suggestionstextfieldkit to 1.0.0
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra af2b2e7aa1 Clean up TODO comments in file provider ui ext
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 395cf9649c Fix SuggestionsTextFieldKit import
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 4464f7e460 Simplify configuration of text field delegate
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 66f77233a8 Fix suggestion labels for sharees
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra db20e44850 Use suggestions window controller in share options view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 07b6391688 Improve logging in ShareeSuggestionsDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 798e060032 Post suggestions changed notification in ShareeSuggestionsDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra c91d5827ab Update suggestions on inputString change in sharee suggestions data source
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ad43c13882 Add converter method fro nksharee to suggestion
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 3012976d9b Add sharee fetcher method to shareesuggestionsdatasource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 74cea5e57a Add basic shareesuggestionsdatasource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra d6e67a1882 Add SuggestionsTextFieldKit dependency
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ca94b452f1 Add package dependency NextcloudCapabilitiesKit to NextcloudIntegration
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra e54652b690 Retry getting information from FileProviderExt if doing so has failed (usually due to opening share view before auth details are present)
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 1f3d636a92 Deduplicate error presentation in ShareTableViewDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra bab3b4181c Fix options view disappearing when clicking create button in share view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra d2242ea9a4 Fix letter used to identify shareability in sharetableviewdatasource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 807371e1b7 Fix scan path to retrieve item metadata in share table view data source
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 9621dede1f Ensure interpolated logging string in share table view data source is public
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 65db197a62 Check if the given item is shareable before fetching shares
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 4d3e63009c Add method to ShareTableViewDataSource to fetch the given item's metadata
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra f7dba3e4c6 Add more debug logging to sharecapabilities
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 3e7cde632e Fix default states around passwords for share options view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 49303045b7 Fix ShareCapabilities parsing
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 7be5541cde Fix password capabilities for public link capabilities
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 3bb6f43bdf Update form layout according to picked type when picked type changed
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ce1bf89a99 Setup the fields in share options view when creating a new form according to capabilities
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra b7541fc783 Update reset to make sure it also resets min and max dates in date picker, don't affect share type
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra b074d19e6e Extract conversion of picked menu item in share type picker into NKShare ShareType into new method
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 3bfbb38e0f Do not bother fetching shares if sharing is disabled on server
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra cfd8c00e94 Fetch sharing capabilities in ShareTableViewDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 8039fcd951 Add init for ShareCapabilities
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 3ffcd6de42 Add init for public link capabilities
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 1e811fead5 Add initialiser from dictionary for EmailCapabilities
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 15651f6a0c Restructure ShareCapabilities
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 2d0cdb3716 Add ShareCapabilities struct
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra b23706633e Adapt visibility of note recipient text field upon toggling checkbox
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra f1adfcf8b7 Adapt visibility of expiration date field upon toggling checkbox
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 4be8bace48 Adapt visibility of password field upon toggling password checkbox
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra d12cdebf48 Reset newly added fields in ShareOptionsView correctly
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra d0195e67c3 Properly handle failure state when creating new share in share options view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 31a9db9e25 Correctly treat share recipient text field in the share options view flow
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra c5d57cde1f Add a text field for relevant shareWith in ShareOptionsView
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 54f3822b3c Have specific cancel behaviour in shareoptionsview on delete if in create mode
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 83ed6ea35f Change deleteButton to a cancel button when in creation mode
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra a7637257d5 Adjust share options view title depending on create mode
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 70cb2bd4c1 Make share options view capable of creating or updating view upon clicking "save" depending on create status
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 4029458eff Connect different share type popup button menu items to outlets in share options view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 131973b935 Make create button in share view controller show options view and toggle create mode
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra e1997bd1fd Add "createMode" toggle to share options view, start building for use to create new shares
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 0ceb6a9481 Add additional options to share controller creation method
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 000fe02bb0 Nilify itemServerRelativePath upon loading item in table view data source
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 8d3b101569 Make item's server relative path public in sharetableviewdatasource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra e25bebcd69 Add static method to create shares in ShareController
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 3a58fbeef6 Add a popupbutton to share options vie to select type of share
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 15f03d6417 Add button to create new share in share view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 2085a1214e Remove unused outlets in tableitemview
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ff305ec9fc Implement deletion functionality for share options view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra a4551b25c6 Improve design of save and delete buttons in share view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 8b285a7ea9 Add deletion capability to sharecontroller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra e52d6dfb23 Properly handle permissions in shareoptionsview
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 49450d52f6 Add convenience property to nkshare extension to see if sharees can edit share file/folder
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 41f229c7c3 Add PermissionValues to NKShare extension
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 7e3769cc45 Blend in-window contents in share view loading overlay
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 4aaaf5b15b Show error in share table view data source if received one in fetch
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 69dfe596e2 Present error for updating share in file provider UI
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra f1d3798396 Add method to dismiss error in share view controller via dismiss button
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 24ce9a1e1e Add method to show error in data source ui delegate
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 9d6db4fa45 Add views to present an error in the share view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 30b4509457 Deselect table view or shares after reload to reset UI
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra a7de8edd16 Fix crash on reloading share table view data source
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 5702b163c8 Implement more logging in sharecontroller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 249dd02e75 Reload data source on share saving
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra f1363040fa Use a sharecontroller in shareviewcontroller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 08917a9559 Make save button functional in share view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra e2b62e492f Add fields as parameters to save in sharecontroller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 3ef88137ad Correctly handle NKShare's "canEdit" in share options view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 19316f4d3a Add convenience function to enable or disable all share option fields
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 422a6c7962 Respond to changes in share of sharecontroller in shareoptionsview
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 755897be55 Add convenience function to format date to valid server string in nkshare
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra f654a8ca83 Use ShareController in ShareOptionsView
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 29262345cd Add functionality to share controller to save changes
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 318661748d Add convenience property to nkshare extension to get valid expiration date string
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra aa57ba10f3 Add starter sharecontroller for fileprovider
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 5e0d990308 Handle optionsview as ShareOptionsView in share view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ec145d8ca2 Handle state of share options view via setting of NKShare
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 52dbfc3109 Improve placeholder text for share view password field
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 322ae529b7 Add a ShareOptionsView class
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 7a40f6f728 Ensure calls are made via main actor to delegate in share table view data source
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 26d635b6fa Tell delegate when fetch ongoing in share table view data source
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra e113a09ecf Add fetch related handling functions to ui delegate
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 55cdbe860b Add a loading overlay to table view to indicate fetch in progress
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 13fa2bca1d Implement hide and show of share options in share view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra c74575cd32 Re-integrate share options view into main share view controller XIB
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 8956fcebe9 Implement ui delegate in ShareTableViewDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 9b1ad4d6d5 Treat bottom view in share view as a target view to inject other views
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra d23f30ae4e Add a shareview ui delegate protocol for datasources
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 1b64e29050 Extract options view to different XIB
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 70a565d60d Redesign shares to incorporate share options into main share view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra d3d75e85f6 Add starter ShareOptionsWindow class
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ecf4efd481 Replace ShareOptionsView with a ShareOptionsWindow
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra cad56f1e2a Implement ShareOptionsView design
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra da8ae80544 Add starter ShareOptionsView
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 18862bed35 Add popover and corresponding view controller to sharetableitemview
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 30673976be Temporarily change copy share link button image after clicking
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 540eb11bf2 Implement share link copying in share table item view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 4f0e25a4e6 Improve share table style
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 18de0d0b3f Make share property central to display update of share table item view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 46e2ca5887 Add displayString property to NKShare extension
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 41f93371a2 Add typeImage property to NKShare extension
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 930106e7ac Add ShareType enum to NKShare extension
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 4ba2bc8a3b Add starter NKShare extension
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra dda5ec295e Customise prepareForReuse in ShareTableItemView
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra b6c247ba8d Ensure usable rowHeight for shares table view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ee21715860 Set basic property of sharetableitemview in delegate method of data source
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra c2ec72c132 Add corresponding class for ShareTableItemView
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 731a5e1f8f Set delegate for shares table view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra f11807815a Remove default item view in ShareViewController table XIB
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ff9e344810 Move all share fetching logic to FileProviderUIExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 331a76195b Register shareItemView nib in shareTableView
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 8add57a048 Implement NSTableViewDelegate viewFor method in sharetableviewdatasource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 765f33b45c Add essential table view data source method to ShareTableViewDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra d53680e583 Add release entitlements fro FileProviderUIExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 145d92e5cb Add method to get an item's server path through FPUIExtensionService
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra e6f20e9498 Remove logging from NextcloudAccount
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 81dc8ce708 Add method to FPUIExtensionServiceSource to get extension account details
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra a6992cf38b Add NextcloudAccount to FileProviderUIExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 259a28e33e Add method to convert dictionary to NextcloudAccount
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 02b5a31eae Add method to export NextcloudAccount details to dictionary
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra adb4028a6b Fix linking of FileProviderUIExt which was missing NextcloudKit symbols
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 5b1b87b788 Add FPUIExtensionServiceSource to services published by FileProviderExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 9ce3af3de7 Fix visibility of logging in ItemSharesController
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra e15db9b938 Fix protocol used for FPUIExtensionServiceSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 46ad6f0bfb Instantiate share data source in share view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 1288448336 Add item shares loading routine to ShareTableViewDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 6dad778498 Add share fetching routine to FPUIExtensionService
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra af93122445 Move serviceConnection method to table view data source
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 8dc9807eb2 Add base properties to ShareTableViewDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 820c7e4bd1 Add outlet for share view table view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra b30f8c30d7 Add starter ShareTableViewDataSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 90f7e3a2e3 Add more complete ShareTableItemView design
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 023e1453ba Add starter ShareTableItemView XIB
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra a586767f00 Add table to ShareViewControlelr XIB
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 07473ac5d9 Add fetch method to itemsharescontroller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 0feb5da08f Add starter ItemSharesController
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 8982857833 Add starter FPUIExtensionServiceSource
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 36a829849b Implement convenience method to acquire FPUIExtensionService in share view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra dbe8a5f8f2 Add starter FPUIExtensionService protocol
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 5fbcec1400 Extract display updating upon url acquisition to different method in ShareViewController
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 09042d701d Set the right filename and icon on share view controller view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 1e34affc01 Add domain property to document action view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 9bb67ee68d Make close button work properly in share view
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 7a9ca59734 Store file provider item identifiers in ShareViewController
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra ce026dfd94 Add proper design for share view in file provider UI
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 13aced88ce Properly add share custom action
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 05e7f1b992 Remove unused IBActions in DocumentActionViewController
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 45b4fee7ba Add convenience method to prepare child view controllers in DocumentActionViewController
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 76045fd989 Create ShareViewController
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra f44fec2ff1 Add logging for prepare methods of document action view controller
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra f35148b1bf Add logger extension to FileProviderUIExt
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra d0baa23b5d Add FileProviderUIExt entitlements file
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 8128697a70 Build and install FileProviderUIExt via CMake
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra 1522c22576 Add new FileProviderUIExt target
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 16:11:50 +08:00
Claudio Cambra fe6f47e3b9
Merge pull request #6635 from nextcloud/bugfix/crash-mac-vfs-toggle
Fix crash when in debug mode when toggling enabled status of an account's virtual files (macOS)
2024-04-17 04:44:27 +08:00
Claudio Cambra 3dcef75c57
Fix crash when in debug mode when toggling enabled status of an account's virtual files (macOS)
Caused by comparing to the wrong thing in the Q_ASSERT *facepalm*

Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2024-04-17 03:05:46 +08:00
Nextcloud bot 3ab692c26e
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-15 02:37:33 +00:00
allexzander adc7a22491
Merge pull request #6613 from nextcloud/feature/office-files-lock-newly-created
Feature/office files lock newly created. Plus refactoring.
2024-04-13 15:58:03 +02:00
alex-z dbde9e3a2b Detect office files for locking on new upload. Notify FolderWatcher.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2024-04-13 15:46:37 +02:00
Matthieu Gallien d6ed67806e
Merge pull request #6472 from nextcloud/nextcloud-dev
Add cmake NEXTCLOUD_DEV so debug client can run in parallel to release client.
2024-04-11 18:43:10 +02:00
Camila Ayres 63b0a9f94d Add cmake NEXTCLOUD_DEV so debug client can run in parallel to release client.
- Defaults to OFF.

Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-04-11 15:42:54 +02:00
Camila Ayres d9d4101ca7
Bump version to 3.13.50.
Signed-off-by: Camila Ayres <hello@camilasan.com>
2024-04-11 15:05:32 +02:00
299 changed files with 9186 additions and 5991 deletions

View File

@ -1,19 +1,19 @@
---
kind: pipeline
name: qt-5.15
name: drone desktop client
steps:
- name: cmake
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
volumes:
- name: build
path: /drone/build
commands:
- cd /drone/build
- cmake -G Ninja -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DADD_E2E_TESTS=ON -DECM_ENABLE_SANITIZERS=address -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 ../src
- cmake /drone/src -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQT_MAJOR_VERSION=6 -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 -DADD_E2E_TESTS=ON
- name: compile
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
volumes:
- name: build
path: /drone/build
@ -22,25 +22,27 @@ steps:
- ninja
- name: test
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
volumes:
- name: build
path: /drone/build
commands:
- cd /drone/build
- ../src/admin/test/wait_for_server.sh "server"
- useradd -m -s /bin/bash test
- chown -R test:test .
- su -c 'ASAN_OPTIONS=detect_odr_violation=0,detect_leaks=0 xvfb-run ctest --output-on-failure' test
- su -c 'xvfb-run ctest --output-on-failure' test
services:
- name: server
image: ghcr.io/nextcloud/continuous-integration-server:latest # also change in updateScreenshots.sh
image: ghcr.io/nextcloud/continuous-integration-shallow-server:latest # also change in updateScreenshots.sh
environment:
EVAL: true
SERVER_VERSION: 'stable24'
SERVER_VERSION: 'stable28'
commands:
- BRANCH="$SERVER_VERSION" /usr/local/bin/initnc.sh
- echo 127.0.0.1 server >> /etc/hosts
- apt-get update && apt-get install -y composer
- su www-data -c "OC_PASS=user1 php /var/www/html/occ user:add --password-from-env --display-name='User One' user1"
- su www-data -c "OC_PASS=user2 php /var/www/html/occ user:add --password-from-env --display-name='User Two' user2"
- su www-data -c "OC_PASS=user3 php /var/www/html/occ user:add --password-from-env --display-name='User Three' user3"
@ -48,12 +50,15 @@ services:
- su www-data -c "php /var/www/html/occ group:add users"
- su www-data -c "php /var/www/html/occ group:adduser users user1"
- su www-data -c "php /var/www/html/occ group:adduser users user2"
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/"
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/"
- su www-data -c "php /var/www/html/occ app:enable activity"
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/"
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/"
- su www-data -c "php /var/www/html/occ app:enable text"
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/"
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/"
- su www-data -c "php /var/www/html/occ app:enable end_to_end_encryption"
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/photos.git /var/www/html/apps/photos/"
- su www-data -c "cd /var/www/html/apps/photos; composer install"
- su www-data -c "php /var/www/html/occ app:enable -f photos"
- /usr/local/bin/run.sh
volumes:
@ -74,15 +79,15 @@ name: qt-5.15-clang
steps:
- name: cmake
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
volumes:
- name: build
path: /drone/build
commands:
- cd /drone/build
- cmake -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_C_COMPILER=clang-14 -DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DADD_E2E_TESTS=ON -DECM_ENABLE_SANITIZERS=address -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 ../src
- cmake /drone/src -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=clang-14 -DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_BUILD_TYPE=Debug -DQT_MAJOR_VERSION=6 -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 -DADD_E2E_TESTS=ON
- name: compile
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
volumes:
- name: build
path: /drone/build
@ -90,25 +95,27 @@ steps:
- cd /drone/build
- ninja
- name: test
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
volumes:
- name: build
path: /drone/build
commands:
- cd /drone/build
- ../src/admin/test/wait_for_server.sh "server"
- useradd -m -s /bin/bash test
- chown -R test:test .
- su -c 'ASAN_OPTIONS=detect_odr_violation=0,detect_leaks=0 xvfb-run ctest --output-on-failure' test
- su -c 'xvfb-run ctest --output-on-failure' test
services:
- name: server
image: ghcr.io/nextcloud/continuous-integration-server:latest # also change in updateScreenshots.sh
image: ghcr.io/nextcloud/continuous-integration-shallow-server:latest # also change in updateScreenshots.sh
environment:
EVAL: true
SERVER_VERSION: 'stable24'
SERVER_VERSION: 'stable28'
commands:
- BRANCH="$SERVER_VERSION" /usr/local/bin/initnc.sh
- echo 127.0.0.1 server >> /etc/hosts
- apt-get update && apt-get install -y composer
- su www-data -c "OC_PASS=user1 php /var/www/html/occ user:add --password-from-env --display-name='User One' user1"
- su www-data -c "OC_PASS=user2 php /var/www/html/occ user:add --password-from-env --display-name='User Two' user2"
- su www-data -c "OC_PASS=user3 php /var/www/html/occ user:add --password-from-env --display-name='User Three' user3"
@ -116,12 +123,15 @@ services:
- su www-data -c "php /var/www/html/occ group:add users"
- su www-data -c "php /var/www/html/occ group:adduser users user1"
- su www-data -c "php /var/www/html/occ group:adduser users user2"
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/"
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/"
- su www-data -c "php /var/www/html/occ app:enable activity"
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/"
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/"
- su www-data -c "php /var/www/html/occ app:enable text"
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/"
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/"
- su www-data -c "php /var/www/html/occ app:enable end_to_end_encryption"
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/photos.git /var/www/html/apps/photos/"
- su www-data -c "cd /var/www/html/apps/photos; composer install"
- su www-data -c "php /var/www/html/occ app:enable -f photos"
- /usr/local/bin/run.sh
volumes:
@ -142,14 +152,14 @@ name: AppImage
steps:
- name: build
image: ghcr.io/nextcloud/continuous-integration-client-appimage:client-appimage-10
image: ghcr.io/nextcloud/continuous-integration-client-appimage-qt6:client-appimage-6.6.3-2
environment:
CI_UPLOAD_GIT_TOKEN:
from_secret: CI_UPLOAD_GIT_TOKEN
CI_UPLOAD_GIT_USERNAME:
from_secret: CI_UPLOAD_GIT_USERNAME
commands:
- BUILDNR=$DRONE_BUILD_NUMBER VERSION_SUFFIX=$DRONE_PULL_REQUEST BUILD_UPDATER=ON DESKTOP_CLIENT_ROOT=$DRONE_WORKSPACE /bin/bash -c "./admin/linux/build-appimage.sh"
- BUILDNR=$DRONE_BUILD_NUMBER VERSION_SUFFIX=$DRONE_PULL_REQUEST BUILD_UPDATER=ON DESKTOP_CLIENT_ROOT=$DRONE_WORKSPACE EXECUTABLE_NAME=nextcloud QT_BASE_DIR=/opt/qt6.6.3 OPENSSL_ROOT_DIR=/usr/local/lib64 /bin/bash -c "./admin/linux/build-appimage.sh"
- BUILDNR=$DRONE_BUILD_NUMBER VERSION_SUFFIX=$DRONE_PULL_REQUEST DESKTOP_CLIENT_ROOT=$DRONE_WORKSPACE /bin/bash -c "./admin/linux/upload-appimage.sh" || echo "Upload failed, however this is an optional step."
trigger:
branch:
@ -196,6 +206,6 @@ trigger:
- push
---
kind: signature
hmac: a8fd97516ee53ca8c938ad413e030dc7df483f116c4b19b5811e359960b7b44d
hmac: fbdc01c6461fcc32d9ebff4be97340cbb6da5566643b60289504ed86b2a67583
...

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: cpp-linter/cpp-linter-action@v2.10.2
- uses: cpp-linter/cpp-linter-action@v2.11.0
id: linter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -28,6 +28,6 @@ jobs:
steps:
- name: Run check
uses: skjnldsv/block-fixup-merge-action@42d26e1b536ce61e5cf467d65fb76caf4aa85acf # v1
uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

17
.github/workflows/linux-appimage.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Linux Appimage Package
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
build:
name: Linux Appimage Package
runs-on: ubuntu-latest
container: ghcr.io/nextcloud/continuous-integration-client-appimage-qt6:client-appimage-6.6.3-2
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Configure, compile and package
run: |
BUILDNR=${GITHUB_RUN_ID} VERSION_SUFFIX=${GITHUB_HEAD_REF} BUILD_UPDATER=ON DESKTOP_CLIENT_ROOT=`pwd` EXECUTABLE_NAME=nextcloud QT_BASE_DIR=/opt/qt6.6.3 OPENSSL_ROOT_DIR=/usr/local/lib64 /bin/bash -c "./admin/linux/build-appimage.sh"
BUILDNR=${GITHUB_RUN_ID} VERSION_SUFFIX=${GITHUB_HEAD_REF} DESKTOP_CLIENT_ROOT=`pwd` /bin/bash -c "./admin/linux/upload-appimage.sh" || echo "Upload failed, however this is an optional step."

View File

@ -6,7 +6,7 @@ jobs:
build:
name: Linux Clang compilation and tests
runs-on: ubuntu-latest
container: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
container: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
steps:
- uses: actions/checkout@v4
with:
@ -15,7 +15,7 @@ jobs:
run: |
mkdir build
cd build
cmake .. -G Ninja -DCMAKE_C_COMPILER=clang-14 -DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64
cmake .. -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=clang-14 -DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_BUILD_TYPE=Debug -DQT_MAJOR_VERSION=6 -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64
ninja
- name: Run tests
run: |

View File

@ -6,7 +6,7 @@ jobs:
build:
name: Linux GCC compilation and tests
runs-on: ubuntu-latest
container: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
container: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
steps:
- uses: actions/checkout@v4
with:
@ -15,7 +15,7 @@ jobs:
run: |
mkdir build
cd build
cmake .. -G Ninja -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64
cmake .. -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQT_MAJOR_VERSION=6 -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64
ninja
- name: Run tests
run: |

View File

@ -5,9 +5,9 @@ on:
jobs:
build:
name: macOS Build and Test
runs-on: macos-latest
runs-on: macos-14
env:
CRAFT_TARGET: macos-64-clang
CRAFT_TARGET: macos-clang-arm64
CRAFT_MASTER_LOCATION: ${{ github.workspace }}/CraftMaster
CRAFT_MASTER_CONFIG: ${{ github.workspace }}/craftmaster.ini
steps:
@ -15,6 +15,12 @@ jobs:
with:
fetch-depth: 1
- name: List Xcode installations
run: sudo ls -1 /Applications | grep "Xcode"
- name: Select Xcode 15.3
run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer
- name: Restore cache
uses: actions/cache@v4
with:

View File

@ -6,7 +6,7 @@ jobs:
build:
name: SonarCloud analysis
runs-on: ubuntu-latest
container: ghcr.io/nextcloud/continuous-integration-client:client-5.15-15
container: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
env:
SONAR_SERVER_URL: "https://sonarcloud.io"
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
@ -25,7 +25,7 @@ jobs:
run: |
mkdir build
cd build
cmake .. -G Ninja -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 -DBUILD_COVERAGE=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake .. -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DQT_MAJOR_VERSION=6 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 -DBUILD_COVERAGE=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} ninja
- name: Run tests
run: |

View File

@ -5,9 +5,9 @@ on:
jobs:
build:
name: Windows Build and Test
runs-on: windows-2019
runs-on: windows-2022
env:
CRAFT_TARGET: windows-msvc2019_64-cl
CRAFT_TARGET: windows-msvc2022_64-cl
COBERTURA_COVERAGE_FILE: ${{ github.workspace }}\cobertura_coverage\coverage.xml
CRAFT_MASTER_LOCATION: ${{ github.workspace }}\CraftMaster
CRAFT_MASTER_CONFIG: ${{ github.workspace }}\craftmaster.ini

View File

@ -23,6 +23,9 @@ set(BIN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
include(${CMAKE_SOURCE_DIR}/NEXTCLOUD.cmake)
set(QT_VERSION_MAJOR "6")
set(REQUIRED_QT_VERSION "6.0.0")
# CfAPI Shell Extensions
set( CFAPI_SHELL_EXTENSIONS_LIB_NAME CfApiShellExtensions )
@ -108,7 +111,8 @@ include(GetGitRevisionDescription)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
add_definitions(
-DQT_DISABLE_DEPRECATED_BEFORE=0x000000
-DQT_DISABLE_DEPRECATED_BEFORE=0x051200
-DQT_DEPRECATED_WARNINGS
-DQT_USE_QSTRINGBUILDER
-DQT_MESSAGELOGCONTEXT #enable function name and line number in debug output
)
@ -138,7 +142,7 @@ if(APPLE AND BUILD_OWNCLOUD_OSX_BUNDLE)
add_definitions(-DBUILD_OWNCLOUD_OSX_BUNDLE)
endif()
find_package(Qt${QT_MAJOR_VERSION} COMPONENTS Core)
option(QUICK_COMPILER "Use QtQuick compiler to improve performance" OFF)
# this option removes Http authentication, keychain, shibboleth etc and is intended for
@ -184,6 +188,9 @@ option(BUILD_GUI "BUILD_GUI" ON)
# build the tests
option(BUILD_TESTING "BUILD_TESTING" ON)
# allows to run nextclouddev in parallel to nextcloud + logs
option(NEXTCLOUD_DEV "NEXTCLOUD_DEV" OFF)
option(ENABLE_CLANG_TIDY "ENABLE_CLANG_TIDY" OFF)
if(ENABLE_CLANG_TIDY)
find_program(CLANG_TIDY_EXE NAMES "clang-tidy")

View File

@ -1,6 +1,15 @@
set( APPLICATION_NAME "Nextcloud" )
set( APPLICATION_SHORTNAME "Nextcloud" )
set( APPLICATION_EXECUTABLE "nextcloud" )
if(NEXTCLOUD_DEV)
set( APPLICATION_NAME "NextcloudDev" )
set( APPLICATION_SHORTNAME "NextDev" )
set( APPLICATION_EXECUTABLE "nextclouddev" )
set( APPLICATION_ICON_NAME "Nextcloud" )
else()
set( APPLICATION_NAME "Nextcloud" )
set( APPLICATION_SHORTNAME "Nextcloud" )
set( APPLICATION_EXECUTABLE "nextcloud" )
set( APPLICATION_ICON_NAME "${APPLICATION_SHORTNAME}" )
endif()
set( APPLICATION_CONFIG_NAME "${APPLICATION_EXECUTABLE}" )
set( APPLICATION_DOMAIN "nextcloud.com" )
set( APPLICATION_VENDOR "Nextcloud GmbH" )
@ -10,8 +19,6 @@ set( APPLICATION_HELP_URL "" CACHE STRING "URL for the help menu" )
if(APPLE AND APPLICATION_NAME STREQUAL "Nextcloud" AND EXISTS "${CMAKE_SOURCE_DIR}/theme/colored/Nextcloud-macOS-icon.svg")
set( APPLICATION_ICON_NAME "Nextcloud-macOS" )
message("Using macOS-specific application icon: ${APPLICATION_ICON_NAME}")
else()
set( APPLICATION_ICON_NAME "${APPLICATION_SHORTNAME}" )
endif()
set( APPLICATION_ICON_SET "SVG" )

View File

@ -1,5 +1,5 @@
set( MIRALL_VERSION_MAJOR 3 )
set( MIRALL_VERSION_MINOR 12 )
set( MIRALL_VERSION_MINOR 13 )
set( MIRALL_VERSION_PATCH 50 )
set( MIRALL_VERSION_YEAR 2024 )
set( MIRALL_SOVERSION 0 )
@ -37,3 +37,5 @@ set( MIRALL_VERSION_STRING "${MIRALL_VERSION}${MIRALL_VERSION_SUFFIX}" )
if( MIRALL_VERSION_BUILD )
set( MIRALL_VERSION_STRING "${MIRALL_VERSION_STRING} (build ${MIRALL_VERSION_BUILD})" )
endif( MIRALL_VERSION_BUILD )
set(QT_MAJOR_VERSION 6)

View File

@ -2,16 +2,13 @@
set -xe
export APPNAME=${APPNAME:-nextcloud}
export APPNAME=${APPNAME:-Nextcloud}
export EXECUTABLE_NAME=${EXECUTABLE_NAME:-nextcloud}
export BUILD_UPDATER=${BUILD_UPDATER:-OFF}
export BUILDNR=${BUILDNR:-0000}
export DESKTOP_CLIENT_ROOT=${DESKTOP_CLIENT_ROOT:-/home/user}
#Set Qt-5.15
export QT_BASE_DIR=/opt/kdeqt5.15
export QTDIR=$QT_BASE_DIR
export PATH=$QT_BASE_DIR/bin:$PATH
export QT_BASE_DIR=${QT_BASE_DIR:-/usr}
export OPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR:-/usr/lib/x86_64-linux-gnu}
# Set defaults
export SUFFIX=${DRONE_PULL_REQUEST:=master}
@ -29,12 +26,14 @@ mkdir build-client
cd build-client
cmake \
-G Ninja \
-D CMAKE_INSTALL_PREFIX=/usr \
-D BUILD_TESTING=OFF \
-D BUILD_UPDATER=$BUILD_UPDATER \
-D MIRALL_VERSION_BUILD=$BUILDNR \
-D MIRALL_VERSION_SUFFIX="$VERSION_SUFFIX" \
-D CMAKE_UNITY_BUILD=ON \
-DCMAKE_PREFIX_PATH=${QT_BASE_DIR} \
-DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR} \
-DCMAKE_INSTALL_PREFIX=/usr \
-DBUILD_TESTING=OFF \
-DBUILD_UPDATER=$BUILD_UPDATER \
-DMIRALL_VERSION_BUILD=$BUILDNR \
-DMIRALL_VERSION_SUFFIX="$VERSION_SUFFIX" \
-DCMAKE_UNITY_BUILD=ON \
${DESKTOP_CLIENT_ROOT}
cmake --build . --target all
DESTDIR=/app cmake --install .
@ -64,48 +63,37 @@ rm -rf etc
# com.nextcloud.desktopclient.nextcloud.desktop
DESKTOP_FILE=$(ls /app/usr/share/applications/*.desktop)
sed -i -e 's|Icon=nextcloud|Icon=Nextcloud|g' ${DESKTOP_FILE} # Bug in desktop file?
cp ./usr/share/icons/hicolor/512x512/apps/*.png . # Workaround for linuxeployqt bug, FIXME
# Because distros need to get their shit together
cp -R /usr/lib/x86_64-linux-gnu/libssl.so* ./usr/lib/
cp -R /usr/lib/x86_64-linux-gnu/libcrypto.so* ./usr/lib/
cp -P /usr/local/lib*/libssl.so* ./usr/lib/
cp -P /usr/local/lib*/libcrypto.so* ./usr/lib/
cp -P /usr/local/lib*/libsqlite*.so* ./usr/lib/
# Use linuxdeploy to deploy
export APPIMAGE_NAME=linuxdeploy-x86_64.AppImage
wget -O ${APPIMAGE_NAME} --ca-directory=/etc/ssl/certs -c "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
chmod a+x ${APPIMAGE_NAME}
./${APPIMAGE_NAME} --appimage-extract
rm ./${APPIMAGE_NAME}
cp -r ./squashfs-root ./linuxdeploy-squashfs-root
# NSS fun
cp -P -r /usr/lib/x86_64-linux-gnu/nss ./usr/lib/
export LD_LIBRARY_PATH=/app/usr/lib:${QT_BASE_DIR}/lib:/usr/local/lib/x86_64-linux-gnu:/usr/local/lib:/usr/local/lib64
./linuxdeploy-squashfs-root/AppRun --desktop-file=${DESKTOP_FILE} --icon-file=usr/share/icons/hicolor/512x512/apps/Nextcloud.png --executable=usr/bin/${EXECUTABLE_NAME} --appdir=AppDir
# Use linuxdeployqt to deploy
LINUXDEPLOYQT_VERSION="continuous"
wget -O linuxdeployqt.AppImage --ca-directory=/etc/ssl/certs -c "https://github.com/probonopd/linuxdeployqt/releases/download/${LINUXDEPLOYQT_VERSION}/linuxdeployqt-continuous-x86_64.AppImage"
chmod a+x linuxdeployqt.AppImage
./linuxdeployqt.AppImage --appimage-extract
rm ./linuxdeployqt.AppImage
cp -r ./squashfs-root ./linuxdeployqt-squashfs-root
unset QTDIR; unset QT_PLUGIN_PATH ; unset LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib/x86_64-linux-gnu
./squashfs-root/AppRun ${DESKTOP_FILE} -bundle-non-qt-libs -qmldir=${DESKTOP_CLIENT_ROOT}/src/gui
# Use linuxdeploy-plugin-qt to deploy qt dependencies
export APPIMAGE_NAME=linuxdeploy-plugin-qt-x86_64.AppImage
wget -O ${APPIMAGE_NAME} --ca-directory=/etc/ssl/certs -c "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
chmod a+x ${APPIMAGE_NAME}
./${APPIMAGE_NAME} --appimage-extract
rm ./${APPIMAGE_NAME}
cp -r ./squashfs-root ./linuxdeploy-plugin-qt-squashfs-root
# Set origin
./squashfs-root/usr/bin/patchelf --set-rpath '$ORIGIN/' /app/usr/lib/lib*sync.so.0
export PATH=${QT_BASE_DIR}/bin:${PATH}
export QML_SOURCES_PATHS=${DESKTOP_CLIENT_ROOT}/src/gui
./linuxdeploy-plugin-qt-squashfs-root/AppRun --appdir=AppDir
# Build AppImage
./squashfs-root/AppRun ${DESKTOP_FILE} -appimage -updateinformation="gh-releases-zsync|nextcloud-releases|desktop|latest|Nextcloud-*-x86_64.AppImage.zsync"
# Workaround issue #103
rm -rf ./squashfs-root
APPIMAGE=$(ls *.AppImage)
"./${APPIMAGE}" --appimage-extract
rm "./${APPIMAGE}"
rm ./squashfs-root/usr/lib/libglib-2.0.so.0
rm ./squashfs-root/usr/lib/libgobject-2.0.so.0
PATH=./linuxdeployqt-squashfs-root/usr/bin:$PATH appimagetool -n ./squashfs-root "$APPIMAGE"
./linuxdeploy-squashfs-root/AppRun --desktop-file=${DESKTOP_FILE} --icon-file=usr/share/icons/hicolor/512x512/apps/Nextcloud.png --executable=usr/bin/${EXECUTABLE_NAME} --appdir=AppDir --output appimage
#move AppImage
if [ ! -z "$DRONE_COMMIT" ]
then
mv *.AppImage ${APPNAME}-${SUFFIX}-${DRONE_COMMIT}-x86_64.AppImage
mv *.AppImage ${EXECUTABLE_NAME}-${SUFFIX}-${DRONE_COMMIT}-x86_64.AppImage
else
mv *.AppImage ${EXECUTABLE_NAME}-${SUFFIX}-x86_64.AppImage
fi
mv *.AppImage ${DESKTOP_CLIENT_ROOT}/

View File

@ -9,7 +9,7 @@ else()
set(MAC_INSTALLER_DO_CUSTOM_BACKGROUND "0")
endif()
find_package(Qt5 5.15 COMPONENTS Core REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} COMPONENTS Core REQUIRED)
configure_file(create_mac.sh.cmake ${CMAKE_CURRENT_BINARY_DIR}/create_mac.sh)
configure_file(macosx.pkgproj.cmake ${CMAKE_CURRENT_BINARY_DIR}/macosx.pkgproj)
configure_file(pre_install.sh.cmake ${CMAKE_CURRENT_BINARY_DIR}/pre_install.sh)

View File

@ -10,6 +10,4 @@ if [ -x "$(command -v pluginkit)" ]; then
pluginkit -e use -i @APPLICATION_REV_DOMAIN@.FinderSyncExt
fi
open -a @APPLICATION_NAME@.app
exit 0

25
admin/test/wait_for_server.sh Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-FileCopyrightText: 2019-2020 Tobias Kaminsky <tobias@kaminsky.me>
# SPDX-License-Identifier: AGPL-3.0-or-later
counter=0
status=""
until [[ $status = "false" ]]; do
status=$(curl 2>/dev/null "http://$1/status.php" | jq .maintenance)
echo "($counter) $status"
if [[ "$status" =~ "false" || "$status" = "" ]]; then
let "counter += 1"
if [[ $counter -gt 90 ]]; then
echo "Failed to wait for server"
exit 1
fi
fi
sleep 10
done

View File

@ -194,14 +194,14 @@
</Component>
<!-- Register URI handler -->
<Component Id="RegistryUriHandler" Guid="*" Win64="$(var.PlatformWin64)">
<RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
<RegistryKey Root="HKLM" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
<RegistryValue Type="string" Value="URL:$(var.AppName) Protocol" />
<RegistryValue Type="string" Name="URL Protocol" Value="" />
</RegistryKey>
<RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\DefaultIcon" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
<RegistryKey Root="HKLM" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\DefaultIcon" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
<RegistryValue Type="string" Value="[INSTALLDIR]$(var.AppExe)" />
</RegistryKey>
<RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\shell\open\command" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
<RegistryKey Root="HKLM" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\shell\open\command" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
<RegistryValue Type="string" Value="&quot;[INSTALLDIR]$(var.AppExe)&quot; &quot;%1&quot;" />
</RegistryKey>
</Component>

View File

@ -61,4 +61,6 @@
#cmakedefine CFAPI_SHELL_EXTENSIONS_LIB_NAME "@CFAPI_SHELL_EXTENSIONS_LIB_NAME@"
#cmakedefine01 NEXTCLOUD_DEV
#endif

View File

@ -17,10 +17,6 @@ CreateCache = False
General/MacDeploymentTarget = 12.0
## This is the location of your python installation.
## This value must be set.
Paths/Python = C:\Python312-x64
Compile/BuildType = RelWithDebInfo
Compile/UseNinja = True
@ -29,7 +25,7 @@ Paths/downloaddir = ${Variables:Root}\downloads
ShortPath/Enabled = False
ShortPath/EnableJunctions = False
; Packager/RepositoryUrl = https://files.kde.org/craft/
Packager/RepositoryUrl = https://files.kde.org/craft/Qt6
Packager/PackageType = NullsoftInstallerPackager
ContinuousIntegration/Enabled = True
@ -42,40 +38,22 @@ Packager/UseCache = ${Variables:UseCache}
Packager/CreateCache = ${Variables:CreateCache}
Packager/CacheDir = ${Variables:Root}\cache
#CodeSigning/Enabled = ${Env:SIGN_PACKAGE}
#CodeSigning/Protected = True
#CodeSigning/Certificate = ${Env:CRAFT_CODESIGN_CERTIFICATE}
#CodeSigning/CommonName =
#CodeSigning/MacDeveloperId = ownCloud GmbH (4AP2STM4H5)
#CodeSigning/MacKeychainPath = sign-${Env:DRONE_BUILD_NUMBER}.keychain
[BlueprintSettings]
# don't try to pip install on the ci
python-modules.ignored = True
dev-utils/python2.ignored = True
dev-utils/python3.ignored = True
nextcloud-client.buildTests = True
binary/mysql.useMariaDB = False
[windows-msvc2019_64-cl]
QtSDK/Compiler = msvc2019_64
General/ABI = windows-msvc2019_64-cl
[windows-msvc2022_64-cl]
QtSDK/Compiler = msvc2022_64
General/ABI = windows-msvc2022_64-cl
Paths/Python = C:\Python312-x64
[macos-64-clang]
General/ABI = macos-64-clang
# Packager/PackageType = MacPkgPackager
[macos-64-clang-debug]
General/ABI = macos-64-clang
Compile/BuildType = Debug
[macos-clang-arm64]
General/ABI = macos-clang-arm64
[macos-clang-arm64-debug]
General/ABI = macos-clang-arm64
Compile/BuildType = Debug
Paths/Python = /Users/runner/hostedtoolcache/Python/3.12.3/arm64
[Env]
CRAFT_CODESIGN_CERTIFICATE =
SIGN_PACKAGE = False
SIGN_PACKAGE = False

View File

@ -128,7 +128,7 @@ Then, in Terminal:
.. code-block:: bash
% brew install git qt qtkeychain cmake openssl glib cmocka
% brew install git qt qtkeychain cmake openssl glib cmocka karchive
5. Certain Homebrew packages are not automatically linked in places where
the build scripts can find them, so you can create a shell-profile script
@ -136,9 +136,8 @@ Then, in Terminal:
.. code-block:: bash
% echo 'export OPENSSL_ROOT_DIR=$(brew --prefix openssl)' >> ~/.nextcloud_build_variables
% echo 'export QT_PATH=$(brew --prefix qt5)/bin' >> ~/.nextcloud_build_variables
% echo 'export Qt5LinguistTools_DIR=$(brew --prefix qt5)/lib/cmake/Qt5LinguistTools/' >> ~/.nextcloud_build_variables
% echo 'export QT_PATH=$(brew --prefix qt6)/bin' >> ~/.nextcloud_build_variables
% echo 'export CMAKE_PREFIX_PATH=$(brew --prefix qt6);$(brew --prefix karchive)' >> ~/.nextcloud_build_variables
.. note:: The name ``~/.nextcloud_build_variables`` is just a suggestion for
convenience. You can use a different file or create an entire shell
@ -207,58 +206,88 @@ Then, in Terminal:
Windows Development Build
-------------------------
If you want to test some changes and deploy them locally, you can build natively
on Windows using MinGW. If you want to generate an installer for deployment, please
follow `Windows Installer Build (Cross-Compile)`_ instead.
System requirements
-------------------
- Windows 10 or Windows 11
- `The desktop client code <https://github.com/nextcloud/desktop>`_
- Python 3
- PowerShell
- Microsoft Visual Studio 2022 and tools to compile C++
- `KDE Craft <https://community.kde.org/Craft>`_
1. Get the required dependencies:
Setting up Microsoft Visual Studio
----------------------------------
* Make sure that you have CMake_ and Git_.
* Download the Qt_ MinGW package. You will use the MinGW version bundled with it.
* Download an `OpenSSL Windows Build`_ (the non-"Light" version)
1. Click on 'Modify' in the Visual Studio Installer:
2. Get the QtKeychain_ sources as well as the latest versions of the Nextcloud client
from Git as follows
.. image:: ./images/building/visual-studio-installer.png
:alt: Visual Studio Installer
.. code-block:: bash
git clone https://github.com/frankosterfeld/qtkeychain.git
git clone git://github.com/nextcloud/client.git
2. Select 'Desktop development with C++'
3. Open the Qt MinGW shortcut console from the Start Menu
.. image:: ./images/building/desktop-development-with-cpp.png
:alt: Desktop development with C++
4. Make sure that OpenSSL's ``bin`` directory as well as your qtkeychain source
directories are in your PATH. This will allow CMake to find the library and
headers, as well as allow the Nextcloud client to find the DLLs at runtime::
Handling the dependencies
-------------------------
We handle the dependencies using `KDE Craft <https://community.kde.org/Craft>`_ because it is easy to set it up and it makes the maintenance much more reliable in all platforms.
set PATH=C:\<OpenSSL Install Dir>\bin;%PATH%
set PATH=C:\<qtkeychain Clone Dir>;%PATH%
1. Set up KDE Craft as instructed in `Get Involved/development/Windows - KDE Community Wiki <https://community.kde.org/Get_Involved/development/Windows>`_ - it requires Python 3 and PowerShell.
2. After running:
5. Build qtkeychain **directly in the source directory** so that the DLL is built
in the same directory as the headers to let CMake find them together through PATH::
.. code-block:: winbatch
cd <qtkeychain Clone Dir>
cmake -G "MinGW Makefiles" .
mingw32-make
cd ..
C:\CraftRoot\craft\craftenv.ps1
6. Create the build directory::
3. Add the `desktop client blueprints <https://github.com/nextcloud/desktop-client-blueprints>`_ - the instructions to handle the client dependencies:
mkdir client-build
cd client-build
.. code-block:: winbatch
7. Build the client::
craft --add-blueprint-repository [git]https://github.com/nextcloud/desktop-client-blueprints.git
craft craft
cmake -G "MinGW Makefiles" ../client
mingw32-make
4. Install all client dependencies:
.. note:: You can try using ninja to build in parallel using
``cmake -G Ninja ../client`` and ``ninja`` instead.
.. note:: Refer to the :ref:`generic-build-instructions` section for additional options.
.. code-block:: winbatch
The Nextcloud binary will appear in the ``bin`` directory.
craft --install-deps nextcloud-client
.. _`Windows Installer Build (Cross-Compile)`:
Compiling
---------
1. Make sure your environment variable %PATH% has no conflicting information to the environment you will use to compile the client. For instance, if you have installed OpenSSL previously and have added it to %PATH%, the OpenSSL installed might be a different version than what was installed via KDE Craft.
2. Open the Command Prompt (cmd.exe)
3. Run:
.. code-block:: winbatch
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
4. To use the tools installed with Visual Studio, you need the following in your %PATH%:
.. image:: ./images/building/path.png
:alt: Windows environment variables
5. Alternatively you can use the tools installed with KDE Craft by adding them to %PATH%:
.. code-block:: winbatch
set "PATH=C:\CraftRoot\bin;C:\CraftRoot\dev-utils\bin;%PATH%"
.. note::
C:\CraftRoot is the path used by default by KDE Craft. When you are setting it up you may choose a different folder.
6. Create build folder, run cmake, compile and install:
.. code-block:: winbatch
cd <desktop-repo-path>
mkdir build
cd build
cmake .. -G Ninja -DCMAKE_INSTALL_PREFIX=. -DCMAKE_PREFIX_PATH=C:\CraftRoot -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build . --target install
7. Now you can use `Qt Creator <https://doc.qt.io/qtcreator>`_ to import the build folder with its configurations to be able to work with the code.
Windows Installer (i.e. Deployment) Build (Cross-Compile)
---------------------------------------------------------
@ -355,7 +384,7 @@ To build the most up-to-date version of the client:
.. note:: qtkeychain must be compiled with the same prefix e.g ``CMAKE_INSTALL_PREFIX=/Users/path/to/client/install/ .``
.. note:: Example:: ``cmake -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5 -DCMAKE_INSTALL_PREFIX=/Users/path/to/client/install/``
.. note:: Example:: ``cmake -DCMAKE_PREFIX_PATH=/usr/local/opt/qt6 -DCMAKE_INSTALL_PREFIX=/Users/path/to/client/install/``
4. Call ``make``.
@ -371,8 +400,7 @@ The following are known cmake parameters:
You need to compile QtKeychain with the same Qt version.
* ``WITH_DOC=TRUE``: Creates doc and manpages through running ``make``; also adds install statements,
providing the ability to install using ``make install``.
* ``CMAKE_PREFIX_PATH=/path/to/Qt5.2.0/5.2.0/yourarch/lib/cmake/``: Builds using Qt5.
* ``BUILD_WITH_QT4=ON``: Builds using Qt4 (even if Qt5 is found).
* ``CMAKE_PREFIX_PATH=/path/to/Qt6/6.7.0/yourarch/lib/cmake/``: Builds using Qt6.
* ``CMAKE_INSTALL_PREFIX=path``: Set an install prefix. This is mandatory on Mac OS
Address Sanitizer

View File

@ -1,15 +1,40 @@
FAQ
===
How the "Edit locally" functionality works when running the desktop client AppImage
-----------------------------------------------------------------------------------
How the "Edit locally" functionality works
------------------------------------------
This functionality depends on the desktop client ability to register the mime to handle the nc:// scheme. That is the handler used by the server to open a file locally. This will allow the desktop client to open a document with the local editor when you click on the option "Edit locally" in your Nextcloud instance.
This functionality depends on the desktop client ability to register the mime to handle the nc:// scheme. That is the handler used by the server to open a file locally.
.. note::
Without properly registering the mime, independent of the browser and distro being used, the desktop client will fail to open a document with the local editor when you click on the option "Edit locally" in your Nextcloud instance.
The browser will warn you of the failure: "Failed to launch 'nc://...' because the scheme does not have a registered handler."
We use AppImage due to its universal compatibility but to take full advantage of the desktop client features you will need a third part software to integrate the AppImage in your system.
We have tested `AppImageLauncher <https://github.com/TheAssassin/AppImageLauncher>`_ and alternatively there is `Go AppImage <https://github.com/probonopd/go-appimage>`_.
How to enable it
^^^^^^^^^^^^^^^^^
Without it, independent of the browser and distro being used, the desktop client will fail to open a document with the local editor when you click on the option "Edit locally" in your Nextcloud instance.
In order to do that, you need to install the desktop client with the MSI installer on Windows or use a third party software to integrate the AppImage in your system on Linux.
On Linux
^^^^^^^^
We use AppImage due to its universal compatibility but to take full advantage of the desktop client features you will need a third part software to integrate the AppImage in your system: we have tested `AppImageLauncher <https://github.com/TheAssassin/AppImageLauncher>`_ and alternatively there is `Go AppImage <https://github.com/probonopd/go-appimage>`_.
On Windows
^^^^^^^^^^
The MSI installer will alter your system registry to register the mime to handle the nc:// scheme.
Alternatively, you can manually register the mime to handle the nc:// scheme:
1. Save the following content to a .reg file:
```
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\nc\shell\open\command]
@="\"C:\\Program Files\\Nextcloud\\nextcloud.exe\" \"%1\""
```
2. Double click on the .reg file to import it into the registry.
See https://nextcloud.com/blog/nextcloud-office-release-solves-document-compatibility-overhauls-knowledge-management/ for more information.

View File

@ -33,13 +33,13 @@ download page.
System Requirements
----------------------------------
- Windows 8.1+
- macOS 12+ (64-bit only)
- Linux
- FreeBSD
- Windows 10+ (64-bits only)
- macOS 11.4+ (64-bits only)
- Linux (ubuntu 22.04 or openSUSE 15.5 or ...) (64-bits only)
.. note::
For Linux distributions, we support, if technically feasible, the latest 2 versions per platform and the previous LTS.
For Linux distributions, we support, if technically feasible, the current LTS releases.
For BSD, we support them if technically feasible but we do not test
Customizing the Windows Installation
------------------------------------

View File

@ -35,7 +35,20 @@ if(APPLE)
COMMENT building macOS File Provider extension
VERBATIM)
add_dependencies(mac_overlayplugin mac_fileproviderplugin nextcloud) # for the ownCloud.icns to be generated
add_custom_target( mac_fileprovideruiplugin ALL
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj
-target FileProviderUIExt -configuration ${XCODE_TARGET_CONFIGURATION} "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
"OC_APPLICATION_EXECUTABLE_NAME=${APPLICATION_EXECUTABLE}"
"OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}"
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
DEPENDS mac_fileproviderplugin
COMMENT building macOS File Provider UI extension
VERBATIM)
add_dependencies(mac_overlayplugin mac_fileproviderplugin mac_fileprovideruiplugin nextcloud) # for the ownCloud.icns to be generated
else()
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
endif()
@ -55,6 +68,10 @@ if(APPLE)
install(DIRECTORY ${OSX_PLUGINS_BINARY_DIR}/FileProviderExt.appex
DESTINATION ${OSX_PLUGINS_INSTALL_DIR}
USE_SOURCE_PERMISSIONS)
install(DIRECTORY ${OSX_PLUGINS_BINARY_DIR}/FileProviderUIExt.appex
DESTINATION ${OSX_PLUGINS_INSTALL_DIR}
USE_SOURCE_PERMISSIONS)
endif()
endif()
endif()

View File

@ -1,195 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Foundation
import OSLog
extension NextcloudFilesDatabaseManager {
func directoryMetadata(account: String, serverUrl: String) -> NextcloudItemMetadataTable? {
// We want to split by "/" (e.g. cloud.nc.com/files/a/b) but we need to be mindful of "https://c.nc.com"
let problematicSeparator = "://"
let placeholderSeparator = "__TEMP_REPLACE__"
let serverUrlWithoutPrefix = serverUrl.replacingOccurrences(
of: problematicSeparator, with: placeholderSeparator)
var splitServerUrl = serverUrlWithoutPrefix.split(separator: "/")
let directoryItemFileName = String(splitServerUrl.removeLast())
let directoryItemServerUrl = splitServerUrl.joined(separator: "/").replacingOccurrences(
of: placeholderSeparator, with: problematicSeparator)
if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@ AND fileName == %@ AND directory == true",
account,
directoryItemServerUrl,
directoryItemFileName
).first {
return NextcloudItemMetadataTable(value: metadata)
}
return nil
}
func childItemsForDirectory(_ directoryMetadata: NextcloudItemMetadataTable)
-> [NextcloudItemMetadataTable]
{
let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"serverUrl BEGINSWITH %@", directoryServerUrl)
return sortedItemMetadatas(metadatas)
}
func childDirectoriesForDirectory(_ directoryMetadata: NextcloudItemMetadataTable)
-> [NextcloudItemMetadataTable]
{
let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"serverUrl BEGINSWITH %@ AND directory == true", directoryServerUrl)
return sortedItemMetadatas(metadatas)
}
func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable)
-> NextcloudItemMetadataTable?
{
directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
}
func directoryMetadata(ocId: String) -> NextcloudItemMetadataTable? {
if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@ AND directory == true", ocId
).first {
return NextcloudItemMetadataTable(value: metadata)
}
return nil
}
func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND directory == true", account)
return sortedItemMetadatas(metadatas)
}
func directoryMetadatas(account: String, parentDirectoryServerUrl: String)
-> [NextcloudItemMetadataTable]
{
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND parentDirectoryServerUrl == %@ AND directory == true", account,
parentDirectoryServerUrl)
return sortedItemMetadatas(metadatas)
}
// Deletes all metadatas related to the info of the directory provided
func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) -> [NextcloudItemMetadataTable]? {
let database = ncDatabase()
guard
let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@ AND directory == true", ocId
).first
else {
Logger.ncFilesDatabase.error(
"Could not find directory metadata for ocId \(ocId, privacy: .public). Not proceeding with deletion"
)
return nil
}
let directoryMetadataCopy = NextcloudItemMetadataTable(value: directoryMetadata)
let directoryUrlPath = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
let directoryAccount = directoryMetadata.account
let directoryEtag = directoryMetadata.etag
Logger.ncFilesDatabase.debug(
"Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)"
)
guard deleteItemMetadata(ocId: directoryMetadata.ocId) else {
Logger.ncFilesDatabase.debug(
"Failure to delete root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)"
)
return nil
}
var deletedMetadatas: [NextcloudItemMetadataTable] = [directoryMetadataCopy]
let results = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryAccount, directoryUrlPath)
for result in results {
let successfulItemMetadataDelete = deleteItemMetadata(ocId: result.ocId)
if successfulItemMetadataDelete {
deletedMetadatas.append(NextcloudItemMetadataTable(value: result))
}
if localFileMetadataFromOcId(result.ocId) != nil {
deleteLocalFileMetadata(ocId: result.ocId)
}
}
Logger.ncFilesDatabase.debug(
"Completed deletions in directory recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)"
)
return deletedMetadatas
}
func renameDirectoryAndPropagateToChildren(
ocId: String, newServerUrl: String, newFileName: String
) -> [NextcloudItemMetadataTable]? {
let database = ncDatabase()
guard
let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@ AND directory == true", ocId
).first
else {
Logger.ncFilesDatabase.error(
"Could not find a directory with ocID \(ocId, privacy: .public), cannot proceed with recursive renaming"
)
return nil
}
let oldItemServerUrl = directoryMetadata.serverUrl
let oldDirectoryServerUrl = oldItemServerUrl + "/" + directoryMetadata.fileName
let newDirectoryServerUrl = newServerUrl + "/" + newFileName
let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account,
oldDirectoryServerUrl)
renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName)
Logger.ncFilesDatabase.debug("Renamed root renaming directory")
do {
try database.write {
for childItem in childItemResults {
let oldServerUrl = childItem.serverUrl
let movedServerUrl = oldServerUrl.replacingOccurrences(
of: oldDirectoryServerUrl, with: newDirectoryServerUrl)
childItem.serverUrl = movedServerUrl
database.add(childItem, update: .all)
Logger.ncFilesDatabase.debug(
"Moved childItem at \(oldServerUrl) to \(movedServerUrl)")
}
}
} catch {
Logger.ncFilesDatabase.error(
"Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)"
)
return nil
}
let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account,
newDirectoryServerUrl)
return sortedItemMetadatas(updatedChildItemResults)
}
}

View File

@ -1,105 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Foundation
import OSLog
import RealmSwift
extension NextcloudFilesDatabaseManager {
func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter(
"ocId == %@", ocId
).first {
return NextcloudLocalFileMetadataTable(value: metadata)
}
return nil
}
func addLocalFileMetadataFromItemMetadata(_ itemMetadata: NextcloudItemMetadataTable) {
let database = ncDatabase()
do {
try database.write {
let newLocalFileMetadata = NextcloudLocalFileMetadataTable()
newLocalFileMetadata.ocId = itemMetadata.ocId
newLocalFileMetadata.fileName = itemMetadata.fileName
newLocalFileMetadata.account = itemMetadata.account
newLocalFileMetadata.etag = itemMetadata.etag
newLocalFileMetadata.exifDate = Date()
newLocalFileMetadata.exifLatitude = "-1"
newLocalFileMetadata.exifLongitude = "-1"
database.add(newLocalFileMetadata, update: .all)
Logger.ncFilesDatabase.debug(
"Added local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: .public)"
)
}
} catch {
Logger.ncFilesDatabase.error(
"Could not add local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
}
}
func deleteLocalFileMetadata(ocId: String) {
let database = ncDatabase()
do {
try database.write {
let results = database.objects(NextcloudLocalFileMetadataTable.self).filter(
"ocId == %@", ocId)
database.delete(results)
}
} catch {
Logger.ncFilesDatabase.error(
"Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
}
}
private func sortedLocalFileMetadatas(_ metadatas: Results<NextcloudLocalFileMetadataTable>)
-> [NextcloudLocalFileMetadataTable]
{
let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) })
}
func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter(
"account == %@", account)
return sortedLocalFileMetadatas(results)
}
func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let localFileMetadatas = localFileMetadatas(account: account)
let localFileMetadatasOcIds = Array(localFileMetadatas.map(\.ocId))
var itemMetadatas: [NextcloudItemMetadataTable] = []
for ocId in localFileMetadatasOcIds {
guard let itemMetadata = itemMetadataFromOcId(ocId) else {
Logger.ncFilesDatabase.error(
"Could not find matching item metadata for local file metadata with ocId: \(ocId, privacy: .public) with request from account: \(account)"
)
continue
}
itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))
}
return itemMetadatas
}
}

View File

@ -1,427 +0,0 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
import NextcloudKit
import OSLog
import RealmSwift
class NextcloudFilesDatabaseManager: NSObject {
static let shared = NextcloudFilesDatabaseManager()
let relativeDatabaseFolderPath = "Database/"
let databaseFilename = "fileproviderextdatabase.realm"
let relativeDatabaseFilePath: String
var databasePath: URL?
let schemaVersion: UInt64 = 100
override init() {
relativeDatabaseFilePath = relativeDatabaseFolderPath + databaseFilename
guard let fileProviderDataDirUrl = pathForFileProviderExtData() else {
super.init()
return
}
databasePath = fileProviderDataDirUrl.appendingPathComponent(relativeDatabaseFilePath)
// Disable file protection for directory DB
// https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm
let dbFolder = fileProviderDataDirUrl.appendingPathComponent(relativeDatabaseFolderPath)
let dbFolderPath = dbFolder.path
do {
try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true)
try FileManager.default.setAttributes(
[
FileAttributeKey.protectionKey: FileProtectionType
.completeUntilFirstUserAuthentication
],
ofItemAtPath: dbFolderPath)
} catch {
Logger.ncFilesDatabase.error(
"Could not set permission level for File Provider database folder, received error: \(error.localizedDescription, privacy: .public)"
)
}
let config = Realm.Configuration(
fileURL: databasePath,
schemaVersion: schemaVersion,
objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self]
)
Realm.Configuration.defaultConfiguration = config
do {
_ = try Realm()
Logger.ncFilesDatabase.info("Successfully started Realm db for FileProviderExt")
} catch let error as NSError {
Logger.ncFilesDatabase.error(
"Error opening Realm db: \(error.localizedDescription, privacy: .public)")
}
super.init()
}
func ncDatabase() -> Realm {
let realm = try! Realm()
realm.refresh()
return realm
}
func anyItemMetadatasForAccount(_ account: String) -> Bool {
!ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account)
.isEmpty
}
func itemMetadataFromOcId(_ ocId: String) -> NextcloudItemMetadataTable? {
// Realm objects are live-fire, i.e. they will be changed and invalidated according to changes in the db
// Let's therefore create a copy
if let itemMetadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", ocId
).first {
return NextcloudItemMetadataTable(value: itemMetadata)
}
return nil
}
func sortedItemMetadatas(_ metadatas: Results<NextcloudItemMetadataTable>)
-> [NextcloudItemMetadataTable]
{
let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
return Array(sortedMetadatas.map { NextcloudItemMetadataTable(value: $0) })
}
func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@", account)
return sortedItemMetadatas(metadatas)
}
func itemMetadatas(account: String, serverUrl: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@", account, serverUrl)
return sortedItemMetadatas(metadatas)
}
func itemMetadatas(
account: String, serverUrl: String, status: NextcloudItemMetadataTable.Status
)
-> [NextcloudItemMetadataTable]
{
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@ AND status == %@",
account,
serverUrl,
status.rawValue)
return sortedItemMetadatas(metadatas)
}
func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier)
-> NextcloudItemMetadataTable?
{
let ocId = identifier.rawValue
return itemMetadataFromOcId(ocId)
}
private func processItemMetadatasToDelete(
existingMetadatas: Results<NextcloudItemMetadataTable>,
updatedMetadatas: [NextcloudItemMetadataTable]
) -> [NextcloudItemMetadataTable] {
var deletedMetadatas: [NextcloudItemMetadataTable] = []
for existingMetadata in existingMetadatas {
guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId)
else { continue }
deletedMetadatas.append(metadataToDelete)
Logger.ncFilesDatabase.debug(
"Deleting item metadata during update. ocID: \(existingMetadata.ocId, privacy: .public), etag: \(existingMetadata.etag, privacy: .public), fileName: \(existingMetadata.fileName, privacy: .public)"
)
}
return deletedMetadatas
}
private func processItemMetadatasToUpdate(
existingMetadatas: Results<NextcloudItemMetadataTable>,
updatedMetadatas: [NextcloudItemMetadataTable],
updateDirectoryEtags: Bool
) -> (
newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable],
directoriesNeedingRename: [NextcloudItemMetadataTable]
) {
var returningNewMetadatas: [NextcloudItemMetadataTable] = []
var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
var directoriesNeedingRename: [NextcloudItemMetadataTable] = []
for updatedMetadata in updatedMetadatas {
if let existingMetadata = existingMetadatas.first(where: {
$0.ocId == updatedMetadata.ocId
}) {
if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue,
!existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata)
{
if updatedMetadata.directory {
if updatedMetadata.serverUrl != existingMetadata.serverUrl
|| updatedMetadata.fileName != existingMetadata.fileName
{
directoriesNeedingRename.append(
NextcloudItemMetadataTable(value: updatedMetadata))
updatedMetadata.etag = "" // Renaming doesn't change the etag so reset manually
} else if !updateDirectoryEtags {
updatedMetadata.etag = existingMetadata.etag
}
}
returningUpdatedMetadatas.append(updatedMetadata)
Logger.ncFilesDatabase.debug(
"Updated existing item metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
)
} else {
Logger.ncFilesDatabase.debug(
"Skipping item metadata update; same as existing, or still downloading/uploading. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
)
}
} else { // This is a new metadata
if !updateDirectoryEtags, updatedMetadata.directory {
updatedMetadata.etag = ""
}
returningNewMetadatas.append(updatedMetadata)
Logger.ncFilesDatabase.debug(
"Created new item metadata during update. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
)
}
}
return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
}
func updateItemMetadatas(
account: String,
serverUrl: String,
updatedMetadatas: [NextcloudItemMetadataTable],
updateDirectoryEtags: Bool
) -> (
newMetadatas: [NextcloudItemMetadataTable]?,
updatedMetadatas: [NextcloudItemMetadataTable]?,
deletedMetadatas: [NextcloudItemMetadataTable]?
) {
let database = ncDatabase()
do {
let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@ AND status == %@",
account,
serverUrl,
NextcloudItemMetadataTable.Status.normal.rawValue)
let metadatasToDelete = processItemMetadatasToDelete(
existingMetadatas: existingMetadatas,
updatedMetadatas: updatedMetadatas)
let metadatasToChange = processItemMetadatasToUpdate(
existingMetadatas: existingMetadatas,
updatedMetadatas: updatedMetadatas,
updateDirectoryEtags: updateDirectoryEtags)
var metadatasToUpdate = metadatasToChange.updatedMetadatas
let metadatasToCreate = metadatasToChange.newMetadatas
let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
let metadatasToAdd =
Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) })
+ Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
for metadata in directoriesNeedingRename {
if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(
ocId: metadata.ocId,
newServerUrl: metadata.serverUrl,
newFileName: metadata.fileName)
{
metadatasToUpdate += updatedDirectoryChildren
}
}
try database.write {
for metadata in metadatasToDelete {
// Can't pass copies, we need the originals from the database
database.delete(
ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", metadata.ocId))
}
for metadata in metadatasToAdd {
database.add(metadata, update: .all)
}
}
return (
newMetadatas: metadatasToCreate,
updatedMetadatas: metadatasToUpdate,
deletedMetadatas: metadatasToDelete
)
} catch {
Logger.ncFilesDatabase.error(
"Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)"
)
return (nil, nil, nil)
}
}
func setStatusForItemMetadata(
_ metadata: NextcloudItemMetadataTable,
status: NextcloudItemMetadataTable.Status,
completionHandler: @escaping (_ updatedMetadata: NextcloudItemMetadataTable?) -> Void
) {
let database = ncDatabase()
do {
try database.write {
guard
let result = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", metadata.ocId
).first
else {
Logger.ncFilesDatabase.debug(
"Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)"
)
return
}
result.status = status.rawValue
database.add(result, update: .all)
Logger.ncFilesDatabase.debug(
"Updated status for item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
)
completionHandler(NextcloudItemMetadataTable(value: result))
}
} catch {
Logger.ncFilesDatabase.error(
"Could not update status for item metadata with ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
completionHandler(nil)
}
}
func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
let database = ncDatabase()
do {
try database.write {
database.add(metadata, update: .all)
Logger.ncFilesDatabase.debug(
"Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
)
}
} catch {
Logger.ncFilesDatabase.error(
"Could not add item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
}
}
@discardableResult func deleteItemMetadata(ocId: String) -> Bool {
let database = ncDatabase()
do {
try database.write {
let results = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", ocId)
Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
database.delete(results)
}
return true
} catch {
Logger.ncFilesDatabase.error(
"Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
return false
}
}
func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
let database = ncDatabase()
do {
try database.write {
guard
let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", ocId
).first
else {
Logger.ncFilesDatabase.debug(
"Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: .public)"
)
return
}
let oldFileName = itemMetadata.fileName
let oldServerUrl = itemMetadata.serverUrl
itemMetadata.fileName = newFileName
itemMetadata.fileNameView = newFileName
itemMetadata.serverUrl = newServerUrl
database.add(itemMetadata, update: .all)
Logger.ncFilesDatabase.debug(
"Renamed item \(oldFileName, privacy: .public) to \(newFileName, privacy: .public), moved from serverUrl: \(oldServerUrl, privacy: .public) to serverUrl: \(newServerUrl, privacy: .public)"
)
}
} catch {
Logger.ncFilesDatabase.error(
"Could not rename filename of item metadata with ocID: \(ocId, privacy: .public) to proposed name \(newFileName, privacy: .public) at proposed serverUrl \(newServerUrl, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
}
}
func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable)
-> NSFileProviderItemIdentifier?
{
let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
if metadata.serverUrl == homeServerFilesUrl {
return .rootContainer
}
guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
Logger.ncFilesDatabase.error(
"Could not get item parent directory metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
)
return nil
}
if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.ocId) {
return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
}
Logger.ncFilesDatabase.error(
"Could not get item parent directory item metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
)
return nil
}
}

View File

@ -1,137 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Foundation
import NextcloudKit
extension NextcloudItemMetadataTable {
static func fromNKFile(_ file: NKFile, account: String) -> NextcloudItemMetadataTable {
let metadata = NextcloudItemMetadataTable()
metadata.account = account
metadata.checksums = file.checksums
metadata.commentsUnread = file.commentsUnread
metadata.contentType = file.contentType
if let date = file.creationDate {
metadata.creationDate = date as Date
} else {
metadata.creationDate = file.date as Date
}
metadata.dataFingerprint = file.dataFingerprint
metadata.date = file.date as Date
metadata.directory = file.directory
metadata.downloadURL = file.downloadURL
metadata.e2eEncrypted = file.e2eEncrypted
metadata.etag = file.etag
metadata.favorite = file.favorite
metadata.fileId = file.fileId
metadata.fileName = file.fileName
metadata.fileNameView = file.fileName
metadata.hasPreview = file.hasPreview
metadata.iconName = file.iconName
metadata.mountType = file.mountType
metadata.name = file.name
metadata.note = file.note
metadata.ocId = file.ocId
metadata.ownerId = file.ownerId
metadata.ownerDisplayName = file.ownerDisplayName
metadata.lock = file.lock
metadata.lockOwner = file.lockOwner
metadata.lockOwnerEditor = file.lockOwnerEditor
metadata.lockOwnerType = file.lockOwnerType
metadata.lockOwnerDisplayName = file.lockOwnerDisplayName
metadata.lockTime = file.lockTime
metadata.lockTimeOut = file.lockTimeOut
metadata.path = file.path
metadata.permissions = file.permissions
metadata.quotaUsedBytes = file.quotaUsedBytes
metadata.quotaAvailableBytes = file.quotaAvailableBytes
metadata.richWorkspace = file.richWorkspace
metadata.resourceType = file.resourceType
metadata.serverUrl = file.serverUrl
metadata.sharePermissionsCollaborationServices = file.sharePermissionsCollaborationServices
for element in file.sharePermissionsCloudMesh {
metadata.sharePermissionsCloudMesh.append(element)
}
for element in file.shareType {
metadata.shareType.append(element)
}
metadata.size = file.size
metadata.classFile = file.classFile
// FIXME: iOS 12.0,* don't detect UTI text/markdown, text/x-markdown
if metadata.contentType == "text/markdown" || metadata.contentType == "text/x-markdown",
metadata.classFile == NKCommon.TypeClassFile.unknow.rawValue
{
metadata.classFile = NKCommon.TypeClassFile.document.rawValue
}
if let date = file.uploadDate {
metadata.uploadDate = date as Date
} else {
metadata.uploadDate = file.date as Date
}
metadata.urlBase = file.urlBase
metadata.user = file.user
metadata.userId = file.userId
// Support for finding the correct filename for e2ee files should go here
return metadata
}
static func metadatasFromDirectoryReadNKFiles(
_ files: [NKFile],
account: String,
completionHandler: @escaping (
_ directoryMetadata: NextcloudItemMetadataTable,
_ childDirectoriesMetadatas: [NextcloudItemMetadataTable],
_ metadatas: [NextcloudItemMetadataTable]
) -> Void
) {
var directoryMetadataSet = false
var directoryMetadata = NextcloudItemMetadataTable()
var childDirectoriesMetadatas: [NextcloudItemMetadataTable] = []
var metadatas: [NextcloudItemMetadataTable] = []
let conversionQueue = DispatchQueue(
label: "nkFileToMetadataConversionQueue",
qos: .userInitiated,
attributes: .concurrent)
// appendQueue is a serial queue, not concurrent
let appendQueue = DispatchQueue(label: "metadataAppendQueue", qos: .userInitiated)
let dispatchGroup = DispatchGroup()
for file in files {
if metadatas.isEmpty, !directoryMetadataSet {
let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account)
directoryMetadata = metadata
directoryMetadataSet = true
} else {
conversionQueue.async(group: dispatchGroup) {
let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account)
appendQueue.async(group: dispatchGroup) {
metadatas.append(metadata)
if metadata.directory {
childDirectoriesMetadatas.append(metadata)
}
}
}
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completionHandler(directoryMetadata, childDirectoriesMetadatas, metadatas)
}
}
}

View File

@ -1,219 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
import NextcloudKit
import RealmSwift
class NextcloudItemMetadataTable: Object {
enum Status: Int {
case downloadError = -4
case downloading = -3
case inDownload = -2
case waitDownload = -1
case normal = 0
case waitUpload = 1
case inUpload = 2
case uploading = 3
case uploadError = 4
}
enum SharePermissions: Int {
case readShare = 1
case updateShare = 2
case createShare = 4
case deleteShare = 8
case shareShare = 16
case maxFileShare = 19
case maxFolderShare = 31
}
@Persisted(primaryKey: true) var ocId: String
@Persisted var account = ""
@Persisted var assetLocalIdentifier = ""
@Persisted var checksums = ""
@Persisted var chunk: Bool = false
@Persisted var classFile = ""
@Persisted var commentsUnread: Bool = false
@Persisted var contentType = ""
@Persisted var creationDate = Date()
@Persisted var dataFingerprint = ""
@Persisted var date = Date()
@Persisted var directory: Bool = false
@Persisted var deleteAssetLocalIdentifier: Bool = false
@Persisted var downloadURL = ""
@Persisted var e2eEncrypted: Bool = false
@Persisted var edited: Bool = false
@Persisted var etag = ""
@Persisted var etagResource = ""
@Persisted var favorite: Bool = false
@Persisted var fileId = ""
@Persisted var fileName = ""
@Persisted var fileNameView = ""
@Persisted var hasPreview: Bool = false
@Persisted var iconName = ""
@Persisted var iconUrl = ""
@Persisted var isExtractFile: Bool = false
@Persisted var livePhoto: Bool = false
@Persisted var mountType = ""
@Persisted var name = "" // for unifiedSearch is the provider.id
@Persisted var note = ""
@Persisted var ownerId = ""
@Persisted var ownerDisplayName = ""
@Persisted var lock = false
@Persisted var lockOwner = ""
@Persisted var lockOwnerEditor = ""
@Persisted var lockOwnerType = 0
@Persisted var lockOwnerDisplayName = ""
@Persisted var lockTime: Date?
@Persisted var lockTimeOut: Date?
@Persisted var path = ""
@Persisted var permissions = ""
@Persisted var quotaUsedBytes: Int64 = 0
@Persisted var quotaAvailableBytes: Int64 = 0
@Persisted var resourceType = ""
@Persisted var richWorkspace: String?
@Persisted var serverUrl = "" // For parent directory!!
@Persisted var session = ""
@Persisted var sessionError = ""
@Persisted var sessionSelector = ""
@Persisted var sessionTaskIdentifier: Int = 0
@Persisted var sharePermissionsCollaborationServices: Int = 0
// TODO: Find a way to compare these two below in remote state check
let sharePermissionsCloudMesh = List<String>()
let shareType = List<Int>()
@Persisted var size: Int64 = 0
@Persisted var status: Int = 0
@Persisted var subline: String?
@Persisted var trashbinFileName = ""
@Persisted var trashbinOriginalLocation = ""
@Persisted var trashbinDeletionTime = Date()
@Persisted var uploadDate = Date()
@Persisted var url = ""
@Persisted var urlBase = ""
@Persisted var user = ""
@Persisted var userId = ""
var fileExtension: String {
(fileNameView as NSString).pathExtension
}
var fileNoExtension: String {
(fileNameView as NSString).deletingPathExtension
}
var isRenameable: Bool {
lock
}
var isPrintable: Bool {
if isDocumentViewableOnly {
return false
}
if ["application/pdf", "com.adobe.pdf"].contains(contentType)
|| contentType.hasPrefix("text/")
|| classFile == NKCommon.TypeClassFile.image.rawValue
{
return true
}
return false
}
var isDocumentViewableOnly: Bool {
sharePermissionsCollaborationServices == SharePermissions.readShare.rawValue
&& classFile == NKCommon.TypeClassFile.document.rawValue
}
var isCopyableInPasteboard: Bool {
!isDocumentViewableOnly && !directory
}
var isModifiableWithQuickLook: Bool {
if directory || isDocumentViewableOnly {
return false
}
return contentType == "com.adobe.pdf" || contentType == "application/pdf"
|| classFile == NKCommon.TypeClassFile.image.rawValue
}
var isSettableOnOffline: Bool {
session.isEmpty && !isDocumentViewableOnly
}
var canOpenIn: Bool {
session.isEmpty && !isDocumentViewableOnly && !directory
}
var isDownloadUpload: Bool {
status == Status.inDownload.rawValue || status == Status.downloading.rawValue
|| status == Status.inUpload.rawValue || status == Status.uploading.rawValue
}
var isDownload: Bool {
status == Status.inDownload.rawValue || status == Status.downloading.rawValue
}
var isUpload: Bool {
status == Status.inUpload.rawValue || status == Status.uploading.rawValue
}
override func isEqual(_ object: Any?) -> Bool {
if let object = object as? NextcloudItemMetadataTable {
return fileId == object.fileId && account == object.account && path == object.path
&& fileName == object.fileName
}
return false
}
func isInSameDatabaseStoreableRemoteState(_ comparingMetadata: NextcloudItemMetadataTable)
-> Bool
{
comparingMetadata.etag == etag
&& comparingMetadata.fileNameView == fileNameView
&& comparingMetadata.date == date
&& comparingMetadata.permissions == permissions
&& comparingMetadata.hasPreview == hasPreview
&& comparingMetadata.note == note
&& comparingMetadata.lock == lock
&& comparingMetadata.sharePermissionsCollaborationServices
== sharePermissionsCollaborationServices
&& comparingMetadata.favorite == favorite
}
/// Returns false if the user is lokced out of the file. I.e. The file is locked but by someone else
func canUnlock(as user: String) -> Bool {
!lock || (lockOwner == user && lockOwnerType == 0)
}
func thumbnailUrl(size: CGSize) -> URL? {
guard hasPreview else {
return nil
}
let urlBase = urlBase.urlEncoded!
// Leave the leading slash in webdavUrl
let webdavUrl = urlBase + NextcloudAccount.webDavFilesUrlSuffix + user
let serverFileRelativeUrl =
serverUrl.replacingOccurrences(of: webdavUrl, with: "") + "/" + fileName
let urlString =
"\(urlBase)/index.php/core/preview.png?file=\(serverFileRelativeUrl)&x=\(size.width)&y=\(size.height)&a=1&mode=cover"
return URL(string: urlString)
}
}

View File

@ -1,29 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Foundation
import RealmSwift
class NextcloudLocalFileMetadataTable: Object {
@Persisted(primaryKey: true) var ocId: String
@Persisted var account = ""
@Persisted var etag = ""
@Persisted var exifDate: Date?
@Persisted var exifLatitude = ""
@Persisted var exifLongitude = ""
@Persisted var exifLensModel: String?
@Persisted var favorite: Bool = false
@Persisted var fileName = ""
@Persisted var offline: Bool = false
}

View File

@ -19,15 +19,10 @@ extension Logger {
static let desktopClientConnection = Logger(
subsystem: subsystem, category: "desktopclientconnection")
static let enumeration = Logger(subsystem: subsystem, category: "enumeration")
static let fpUiExtensionService = Logger(subsystem: subsystem, category: "fpUiExtensionService")
static let fileProviderExtension = Logger(
subsystem: subsystem, category: "fileproviderextension")
static let fileTransfer = Logger(subsystem: subsystem, category: "filetransfer")
static let localFileOps = Logger(subsystem: subsystem, category: "localfileoperations")
static let ncFilesDatabase = Logger(subsystem: subsystem, category: "nextcloudfilesdatabase")
static let materialisedFileHandling = Logger(
subsystem: subsystem, category: "materialisedfilehandling"
)
static let shares = Logger(subsystem: subsystem, category: "shares")
static let logger = Logger(subsystem: subsystem, category: "logger")
@available(macOSApplicationExtension 12.0, *)

View File

@ -1,60 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
import NextcloudKit
extension NKError {
static var noChangesErrorCode: Int {
-200
}
var isCouldntConnectError: Bool {
errorCode == -9999 || errorCode == -1001 || errorCode == -1004 || errorCode == -1005
|| errorCode == -1009 || errorCode == -1012 || errorCode == -1200 || errorCode == -1202
|| errorCode == 500 || errorCode == 503 || errorCode == 200
}
var isUnauthenticatedError: Bool {
errorCode == -1013
}
var isGoingOverQuotaError: Bool {
errorCode == 507
}
var isNotFoundError: Bool {
errorCode == 404
}
var isNoChangesError: Bool {
errorCode == NKError.noChangesErrorCode
}
var fileProviderError: NSFileProviderError {
if isNotFoundError {
NSFileProviderError(.noSuchItem)
} else if isCouldntConnectError {
// Provide something the file provider can do something with
NSFileProviderError(.serverUnreachable)
} else if isUnauthenticatedError {
NSFileProviderError(.notAuthenticated)
} else if isGoingOverQuotaError {
NSFileProviderError(.insufficientQuota)
} else {
NSFileProviderError(.cannotSynchronize)
}
}
}

View File

@ -1,57 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Alamofire
import Foundation
extension Progress {
func setHandlersFromAfRequest(_ request: Request) {
cancellationHandler = { request.cancel() }
pausingHandler = { request.suspend() }
resumingHandler = { request.resume() }
}
func copyCurrentStateToProgress(_ otherProgress: Progress, includeHandlers: Bool = false) {
if includeHandlers {
otherProgress.cancellationHandler = cancellationHandler
otherProgress.pausingHandler = pausingHandler
otherProgress.resumingHandler = resumingHandler
}
otherProgress.totalUnitCount = totalUnitCount
otherProgress.completedUnitCount = completedUnitCount
otherProgress.estimatedTimeRemaining = estimatedTimeRemaining
otherProgress.localizedDescription = localizedAdditionalDescription
otherProgress.localizedAdditionalDescription = localizedAdditionalDescription
otherProgress.isCancellable = isCancellable
otherProgress.isPausable = isPausable
otherProgress.fileCompletedCount = fileCompletedCount
otherProgress.fileURL = fileURL
otherProgress.fileTotalCount = fileTotalCount
otherProgress.fileCompletedCount = fileCompletedCount
otherProgress.fileOperationKind = fileOperationKind
otherProgress.kind = kind
otherProgress.throughput = throughput
for (key, object) in userInfo {
otherProgress.setUserInfoObject(object, forKey: key)
}
}
func copyOfCurrentState(includeHandlers: Bool = false) -> Progress {
let newProgress = Progress()
copyCurrentStateToProgress(newProgress, includeHandlers: includeHandlers)
return newProgress
}
}

View File

@ -1,405 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import NextcloudKit
import OSLog
extension FileProviderEnumerator {
func fullRecursiveScan(
ncAccount: NextcloudAccount,
ncKit: NextcloudKit,
scanChangesOnly: Bool,
completionHandler: @escaping (
_ metadatas: [NextcloudItemMetadataTable],
_ newMetadatas: [NextcloudItemMetadataTable],
_ updatedMetadatas: [NextcloudItemMetadataTable],
_ deletedMetadatas: [NextcloudItemMetadataTable],
_ error: NKError?
) -> Void
) {
let rootContainerDirectoryMetadata = NextcloudItemMetadataTable()
rootContainerDirectoryMetadata.directory = true
rootContainerDirectoryMetadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
// Create a serial dispatch queue
let dispatchQueue = DispatchQueue(
label: "recursiveChangeEnumerationQueue", qos: .userInitiated)
dispatchQueue.async {
let results = self.scanRecursively(
rootContainerDirectoryMetadata,
ncAccount: ncAccount,
ncKit: ncKit,
scanChangesOnly: scanChangesOnly)
// Run a check to ensure files deleted in one location are not updated in another (e.g. when moved)
// The recursive scan provides us with updated/deleted metadatas only on a folder by folder basis;
// so we need to check we are not simultaneously marking a moved file as deleted and updated
var checkedDeletedMetadatas = results.deletedMetadatas
for updatedMetadata in results.updatedMetadatas {
guard
let matchingDeletedMetadataIdx = checkedDeletedMetadatas.firstIndex(where: {
$0.ocId == updatedMetadata.ocId
})
else {
continue
}
checkedDeletedMetadatas.remove(at: matchingDeletedMetadataIdx)
}
DispatchQueue.main.async {
completionHandler(
results.metadatas, results.newMetadatas, results.updatedMetadatas,
checkedDeletedMetadatas, results.error)
}
}
}
private func scanRecursively(
_ directoryMetadata: NextcloudItemMetadataTable,
ncAccount: NextcloudAccount,
ncKit: NextcloudKit,
scanChangesOnly: Bool
) -> (
metadatas: [NextcloudItemMetadataTable],
newMetadatas: [NextcloudItemMetadataTable],
updatedMetadatas: [NextcloudItemMetadataTable],
deletedMetadatas: [NextcloudItemMetadataTable],
error: NKError?
) {
if isInvalidated {
return ([], [], [], [], nil)
}
assert(directoryMetadata.directory, "Can only recursively scan a directory.")
// Will include results of recursive calls
var allMetadatas: [NextcloudItemMetadataTable] = []
var allNewMetadatas: [NextcloudItemMetadataTable] = []
var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
let dbManager = NextcloudFilesDatabaseManager.shared
let dispatchGroup = DispatchGroup() // TODO: Maybe own thread?
dispatchGroup.enter()
var criticalError: NKError?
let itemServerUrl =
directoryMetadata.ocId == NSFileProviderItemIdentifier.rootContainer.rawValue
? ncAccount.davFilesUrl : directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
Logger.enumeration.debug("About to read: \(itemServerUrl, privacy: .public)")
FileProviderEnumerator.readServerUrl(
itemServerUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: scanChangesOnly
) { metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
if readError != nil {
let nkReadError = NKError(error: readError!)
// Is the error is that we have found matching etags on this item, then ignore it
// if we are doing a full rescan
guard nkReadError.isNoChangesError, scanChangesOnly else {
Logger.enumeration.error(
"Finishing enumeration of changes at \(itemServerUrl, privacy: .public) with \(readError!.localizedDescription, privacy: .public)"
)
if nkReadError.isNotFoundError {
Logger.enumeration.info(
"404 error means item no longer exists. Deleting metadata and reporting as deletion without error"
)
if let deletedMetadatas =
dbManager.deleteDirectoryAndSubdirectoriesMetadata(
ocId: directoryMetadata.ocId)
{
allDeletedMetadatas += deletedMetadatas
} else {
Logger.enumeration.error(
"An error occurred while trying to delete directory and children not found in recursive scan"
)
}
} else if nkReadError.isNoChangesError { // All is well, just no changed etags
Logger.enumeration.info(
"Error was to say no changed files -- not bad error. No need to check children."
)
} else if nkReadError.isUnauthenticatedError
|| nkReadError.isCouldntConnectError
{
// If it is a critical error then stop, if not then continue
Logger.enumeration.error(
"Error will affect next enumerated items, so stopping enumeration.")
criticalError = nkReadError
}
dispatchGroup.leave()
return
}
}
Logger.enumeration.info(
"Finished reading serverUrl: \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
if let metadatas {
allMetadatas += metadatas
} else {
Logger.enumeration.warning(
"WARNING: Nil metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
}
if let newMetadatas {
allNewMetadatas += newMetadatas
} else {
Logger.enumeration.warning(
"WARNING: Nil new metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
}
if let updatedMetadatas {
allUpdatedMetadatas += updatedMetadatas
} else {
Logger.enumeration.warning(
"WARNING: Nil updated metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
}
if let deletedMetadatas {
allDeletedMetadatas += deletedMetadatas
} else {
Logger.enumeration.warning(
"WARNING: Nil deleted metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
}
dispatchGroup.leave()
}
dispatchGroup.wait()
guard criticalError == nil else {
Logger.enumeration.error(
"Received critical error stopping further scanning: \(criticalError!.errorDescription, privacy: .public)"
)
return ([], [], [], [], error: criticalError)
}
var childDirectoriesToScan: [NextcloudItemMetadataTable] = []
var candidateMetadatas: [NextcloudItemMetadataTable]
if scanChangesOnly, fastEnumeration {
candidateMetadatas = allUpdatedMetadatas
} else if scanChangesOnly {
candidateMetadatas = allUpdatedMetadatas + allNewMetadatas
} else {
candidateMetadatas = allMetadatas
}
for candidateMetadata in candidateMetadatas {
if candidateMetadata.directory {
childDirectoriesToScan.append(candidateMetadata)
}
}
Logger.enumeration.debug("Candidate metadatas for further scan: \(candidateMetadatas, privacy: .public)")
if childDirectoriesToScan.isEmpty {
return (
metadatas: allMetadatas, newMetadatas: allNewMetadatas,
updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil
)
}
for childDirectory in childDirectoriesToScan {
Logger.enumeration.debug(
"About to recursively scan: \(childDirectory.urlBase, privacy: .public) with etag: \(childDirectory.etag, privacy: .public)"
)
let childScanResult = scanRecursively(
childDirectory, ncAccount: ncAccount, ncKit: ncKit, scanChangesOnly: scanChangesOnly
)
allMetadatas += childScanResult.metadatas
allNewMetadatas += childScanResult.newMetadatas
allUpdatedMetadatas += childScanResult.updatedMetadatas
allDeletedMetadatas += childScanResult.deletedMetadatas
}
return (
metadatas: allMetadatas, newMetadatas: allNewMetadatas,
updatedMetadatas: allUpdatedMetadatas,
deletedMetadatas: allDeletedMetadatas, nil
)
}
static func handleDepth1ReadFileOrFolder(
serverUrl: String,
ncAccount: NextcloudAccount,
files: [NKFile],
error: NKError,
completionHandler: @escaping (
_ metadatas: [NextcloudItemMetadataTable]?,
_ newMetadatas: [NextcloudItemMetadataTable]?,
_ updatedMetadatas: [NextcloudItemMetadataTable]?,
_ deletedMetadatas: [NextcloudItemMetadataTable]?,
_ readError: Error?
) -> Void
) {
guard error == .success else {
Logger.enumeration.error(
"1 depth readFileOrFolder of url: \(serverUrl, privacy: .public) did not complete successfully, received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(nil, nil, nil, nil, error.error)
return
}
Logger.enumeration.debug(
"Starting async conversion of NKFiles for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
let dbManager = NextcloudFilesDatabaseManager.shared
DispatchQueue.global(qos: .userInitiated).async {
NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(
files, account: ncAccount.ncKitAccount
) { directoryMetadata, _, metadatas in
// STORE DATA FOR CURRENTLY SCANNED DIRECTORY
// We have now scanned this directory's contents, so update with etag in order to not check again if not needed
// unless it's the root container
if serverUrl != ncAccount.davFilesUrl {
dbManager.addItemMetadata(directoryMetadata)
}
// Don't update the etags for folders as we haven't checked their contents.
// When we do a recursive check, if we update the etags now, we will think
// that our local copies are up to date -- instead, leave them as the old.
// They will get updated when they are the subject of a readServerUrl call.
// (See above)
let changedMetadatas = dbManager.updateItemMetadatas(
account: ncAccount.ncKitAccount, serverUrl: serverUrl,
updatedMetadatas: metadatas,
updateDirectoryEtags: false)
DispatchQueue.main.async {
completionHandler(
metadatas, changedMetadatas.newMetadatas, changedMetadatas.updatedMetadatas,
changedMetadatas.deletedMetadatas, nil)
}
}
}
}
static func readServerUrl(
_ serverUrl: String,
ncAccount: NextcloudAccount,
ncKit: NextcloudKit,
stopAtMatchingEtags: Bool = false,
depth: String = "1",
completionHandler: @escaping (
_ metadatas: [NextcloudItemMetadataTable]?,
_ newMetadatas: [NextcloudItemMetadataTable]?,
_ updatedMetadatas: [NextcloudItemMetadataTable]?,
_ deletedMetadatas: [NextcloudItemMetadataTable]?,
_ readError: Error?
) -> Void
) {
let dbManager = NextcloudFilesDatabaseManager.shared
let ncKitAccount = ncAccount.ncKitAccount
Logger.enumeration.debug(
"Starting to read serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public) at depth \(depth, privacy: .public). NCKit info: userId: \(ncKit.nkCommonInstance.user, privacy: .public), password is empty: \(ncKit.nkCommonInstance.password == "" ? "EMPTY PASSWORD" : "NOT EMPTY PASSWORD"), urlBase: \(ncKit.nkCommonInstance.urlBase, privacy: .public), ncVersion: \(ncKit.nkCommonInstance.nextcloudVersion, privacy: .public)"
)
ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: depth, showHiddenFiles: true) {
_, files, _, error in
guard error == .success else {
Logger.enumeration.error(
"\(depth, privacy: .public) depth readFileOrFolder of url: \(serverUrl, privacy: .public) did not complete successfully, received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(nil, nil, nil, nil, error.error)
return
}
guard let receivedFile = files.first else {
Logger.enumeration.error(
"Received no items from readFileOrFolder of \(serverUrl, privacy: .public), not much we can do..."
)
completionHandler(nil, nil, nil, nil, error.error)
return
}
guard receivedFile.directory else {
Logger.enumeration.debug(
"Read item is a file. Converting NKfile for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
let itemMetadata = NextcloudItemMetadataTable.fromNKFile(
receivedFile, account: ncKitAccount)
dbManager.addItemMetadata(itemMetadata) // TODO: Return some value when it is an update
completionHandler([itemMetadata], nil, nil, nil, error.error)
return
}
if stopAtMatchingEtags,
let directoryMetadata = dbManager.directoryMetadata(
account: ncKitAccount, serverUrl: serverUrl)
{
let directoryEtag = directoryMetadata.etag
guard directoryEtag == "" || directoryEtag != receivedFile.etag else {
Logger.enumeration.debug(
"Read server url called with flag to stop enumerating at matching etags. Returning and providing soft error."
)
let description =
"Fetched directory etag is same as that stored locally. Not fetching child items."
let nkError = NKError(
errorCode: NKError.noChangesErrorCode, errorDescription: description)
let metadatas = dbManager.itemMetadatas(
account: ncKitAccount, serverUrl: serverUrl)
completionHandler(metadatas, nil, nil, nil, nkError.error)
return
}
}
if depth == "0" {
if serverUrl != ncAccount.davFilesUrl {
let metadata = NextcloudItemMetadataTable.fromNKFile(
receivedFile, account: ncKitAccount)
let isNew = dbManager.itemMetadataFromOcId(metadata.ocId) == nil
let updatedMetadatas = isNew ? [] : [metadata]
let newMetadatas = isNew ? [metadata] : []
dbManager.addItemMetadata(metadata)
DispatchQueue.main.async {
completionHandler([metadata], newMetadatas, updatedMetadatas, nil, nil)
}
}
} else {
handleDepth1ReadFileOrFolder(
serverUrl: serverUrl, ncAccount: ncAccount, files: files, error: error,
completionHandler: completionHandler)
}
}
}
}

View File

@ -1,461 +0,0 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import NextcloudKit
import OSLog
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
private let enumeratedItemIdentifier: NSFileProviderItemIdentifier
private var enumeratedItemMetadata: NextcloudItemMetadataTable?
private var enumeratingSystemIdentifier: Bool {
FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier)
}
// TODO: actually use this in NCKit and server requests
private let anchor = NSFileProviderSyncAnchor(Date().description.data(using: .utf8)!)
private static let maxItemsPerFileProviderPage = 100
let ncAccount: NextcloudAccount
let ncKit: NextcloudKit
let fastEnumeration: Bool
var serverUrl: String = ""
var isInvalidated = false
private static func isSystemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> Bool {
identifier == .rootContainer || identifier == .trashContainer || identifier == .workingSet
}
init(
enumeratedItemIdentifier: NSFileProviderItemIdentifier,
ncAccount: NextcloudAccount,
ncKit: NextcloudKit,
fastEnumeration: Bool = true
) {
self.enumeratedItemIdentifier = enumeratedItemIdentifier
self.ncAccount = ncAccount
self.ncKit = ncKit
self.fastEnumeration = fastEnumeration
if FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier) {
Logger.enumeration.debug(
"Providing enumerator for a system defined container: \(enumeratedItemIdentifier.rawValue, privacy: .public)"
)
serverUrl = ncAccount.davFilesUrl
} else {
Logger.enumeration.debug(
"Providing enumerator for item with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)"
)
let dbManager = NextcloudFilesDatabaseManager.shared
enumeratedItemMetadata = dbManager.itemMetadataFromFileProviderItemIdentifier(
enumeratedItemIdentifier)
if enumeratedItemMetadata != nil {
serverUrl =
enumeratedItemMetadata!.serverUrl + "/" + enumeratedItemMetadata!.fileName
} else {
Logger.enumeration.error(
"Could not find itemMetadata for file with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)"
)
}
}
Logger.enumeration.info(
"Set up enumerator for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
super.init()
}
func invalidate() {
Logger.enumeration.debug(
"Enumerator is being invalidated for item with identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)"
)
isInvalidated = true
}
// MARK: - Protocol methods
func enumerateItems(
for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage
) {
Logger.enumeration.debug(
"Received enumerate items request for enumerator with user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
/*
- inspect the page to determine whether this is an initial or a follow-up request (TODO)
If this is an enumerator for a directory, the root container or all directories:
- perform a server request to fetch directory contents
If this is an enumerator for the working set:
- perform a server request to update your local database
- fetch the working set from your local database
- inform the observer about the items returned by the server (possibly multiple times)
- inform the observer that you are finished with this page
*/
if enumeratedItemIdentifier == .trashContainer {
Logger.enumeration.debug(
"Enumerating trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
// TODO!
observer.finishEnumerating(upTo: nil)
return
}
// Handle the working set as if it were the root container
// If we do a full server scan per the recommendations of the File Provider documentation,
// we will be stuck for a huge period of time without being able to access files as the
// entire server gets scanned. Instead, treat the working set as the root container here.
// Then, when we enumerate changes, we'll go through everything -- while we can still
// navigate a little bit in Finder, file picker, etc
guard serverUrl != "" else {
Logger.enumeration.error(
"Enumerator has empty serverUrl -- can't enumerate that! For identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)"
)
observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem))
return
}
// TODO: Make better use of pagination and handle paging properly
if page == NSFileProviderPage.initialPageSortedByDate as NSFileProviderPage
|| page == NSFileProviderPage.initialPageSortedByName as NSFileProviderPage
{
Logger.enumeration.debug(
"Enumerating initial page for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
FileProviderEnumerator.readServerUrl(serverUrl, ncAccount: ncAccount, ncKit: ncKit) {
metadatas, _, _, _, readError in
guard readError == nil else {
Logger.enumeration.error(
"Finishing enumeration for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with error \(readError!.localizedDescription, privacy: .public)"
)
let nkReadError = NKError(error: readError!)
observer.finishEnumeratingWithError(nkReadError.fileProviderError)
return
}
guard let metadatas else {
Logger.enumeration.error(
"Finishing enumeration for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with invalid metadatas."
)
observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize))
return
}
Logger.enumeration.info(
"Finished reading serverUrl: \(self.serverUrl, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public). Processed \(metadatas.count) metadatas"
)
FileProviderEnumerator.completeEnumerationObserver(
observer, ncKit: self.ncKit, numPage: 1, itemMetadatas: metadatas)
}
return
}
let numPage = Int(String(data: page.rawValue, encoding: .utf8)!)!
Logger.enumeration.debug(
"Enumerating page \(numPage, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
// TODO: Handle paging properly
// FileProviderEnumerator.completeObserver(observer, ncKit: ncKit, numPage: numPage, itemMetadatas: nil)
observer.finishEnumerating(upTo: nil)
}
func enumerateChanges(
for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor
) {
Logger.enumeration.debug(
"Received enumerate changes request for enumerator for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
/*
- query the server for updates since the passed-in sync anchor (TODO)
If this is an enumerator for the working set:
- note the changes in your local database
- inform the observer about item deletions and updates (modifications + insertions)
- inform the observer when you have finished enumerating up to a subsequent sync anchor
*/
if enumeratedItemIdentifier == .workingSet {
Logger.enumeration.debug(
"Enumerating changes in working set for user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
// Unlike when enumerating items we can't progressively enumerate items as we need to wait to resolve which items are truly deleted and which
// have just been moved elsewhere.
fullRecursiveScan(
ncAccount: ncAccount,
ncKit: ncKit,
scanChangesOnly: true
) { _, newMetadatas, updatedMetadatas, deletedMetadatas, error in
if self.isInvalidated {
Logger.enumeration.info(
"Enumerator invalidated during working set change scan. For user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize))
return
}
guard error == nil else {
Logger.enumeration.info(
"Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with error: \(error!.errorDescription, privacy: .public)"
)
observer.finishEnumeratingWithError(error!.fileProviderError)
return
}
Logger.enumeration.info(
"Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public). Enumerating items."
)
FileProviderEnumerator.completeChangesObserver(
observer,
anchor: anchor,
ncKit: self.ncKit,
newMetadatas: newMetadatas,
updatedMetadatas: updatedMetadatas,
deletedMetadatas: deletedMetadatas)
}
return
} else if enumeratedItemIdentifier == .trashContainer {
Logger.enumeration.debug(
"Enumerating changes in trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
// TODO!
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
return
}
Logger.enumeration.info(
"Enumerating changes for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
// No matter what happens here we finish enumeration in some way, either from the error
// handling below or from the completeChangesObserver
// TODO: Move to the sync engine extension
FileProviderEnumerator.readServerUrl(
serverUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: true
) { _, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
// If we get a 404 we might add more deleted metadatas
var currentDeletedMetadatas: [NextcloudItemMetadataTable] = []
if let notNilDeletedMetadatas = deletedMetadatas {
currentDeletedMetadatas = notNilDeletedMetadatas
}
guard readError == nil else {
Logger.enumeration.error(
"Finishing enumeration of changes for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with error: \(readError!.localizedDescription, privacy: .public)"
)
let nkReadError = NKError(error: readError!)
let fpError = nkReadError.fileProviderError
if nkReadError.isNotFoundError {
Logger.enumeration.info(
"404 error means item no longer exists. Deleting metadata and reporting \(self.serverUrl, privacy: .public) as deletion without error"
)
guard let itemMetadata = self.enumeratedItemMetadata else {
Logger.enumeration.error(
"Invalid enumeratedItemMetadata, could not delete metadata nor report deletion"
)
observer.finishEnumeratingWithError(fpError)
return
}
let dbManager = NextcloudFilesDatabaseManager.shared
if itemMetadata.directory {
if let deletedDirectoryMetadatas =
dbManager.deleteDirectoryAndSubdirectoriesMetadata(
ocId: itemMetadata.ocId)
{
currentDeletedMetadatas += deletedDirectoryMetadatas
} else {
Logger.enumeration.error(
"Something went wrong when recursively deleting directory not found."
)
}
} else {
dbManager.deleteItemMetadata(ocId: itemMetadata.ocId)
}
FileProviderEnumerator.completeChangesObserver(
observer, anchor: anchor, ncKit: self.ncKit, newMetadatas: nil,
updatedMetadatas: nil,
deletedMetadatas: [itemMetadata])
return
} else if nkReadError.isNoChangesError { // All is well, just no changed etags
Logger.enumeration.info(
"Error was to say no changed files -- not bad error. Finishing change enumeration."
)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
return
}
observer.finishEnumeratingWithError(fpError)
return
}
Logger.enumeration.info(
"Finished reading serverUrl: \(self.serverUrl, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
FileProviderEnumerator.completeChangesObserver(
observer,
anchor: anchor,
ncKit: self.ncKit,
newMetadatas: newMetadatas,
updatedMetadatas: updatedMetadatas,
deletedMetadatas: deletedMetadatas)
}
}
func currentSyncAnchor(completionHandler: @escaping (NSFileProviderSyncAnchor?) -> Void) {
completionHandler(anchor)
}
// MARK: - Helper methods
private static func metadatasToFileProviderItems(
_ itemMetadatas: [NextcloudItemMetadataTable], ncKit: NextcloudKit,
completionHandler: @escaping (_ items: [NSFileProviderItem]) -> Void
) {
var items: [NSFileProviderItem] = []
let conversionQueue = DispatchQueue(
label: "metadataToItemConversionQueue", qos: .userInitiated, attributes: .concurrent)
let appendQueue = DispatchQueue(label: "enumeratorItemAppendQueue", qos: .userInitiated) // Serial queue
let dispatchGroup = DispatchGroup()
for itemMetadata in itemMetadatas {
conversionQueue.async(group: dispatchGroup) {
if itemMetadata.e2eEncrypted {
Logger.enumeration.info(
"Skipping encrypted metadata in enumeration: \(itemMetadata.ocId, privacy: .public) \(itemMetadata.fileName, privacy: .public)"
)
return
}
if let parentItemIdentifier = NextcloudFilesDatabaseManager.shared
.parentItemIdentifierFromMetadata(itemMetadata)
{
let item = FileProviderItem(
metadata: itemMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: ncKit)
Logger.enumeration.debug(
"Will enumerate item with ocId: \(itemMetadata.ocId, privacy: .public) and name: \(itemMetadata.fileName, privacy: .public)"
)
appendQueue.async(group: dispatchGroup) {
items.append(item)
}
} else {
Logger.enumeration.error(
"Could not get valid parentItemIdentifier for item with ocId: \(itemMetadata.ocId, privacy: .public) and name: \(itemMetadata.fileName, privacy: .public), skipping enumeration"
)
}
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completionHandler(items)
}
}
private static func fileProviderPageforNumPage(_ numPage: Int) -> NSFileProviderPage {
NSFileProviderPage("\(numPage)".data(using: .utf8)!)
}
private static func completeEnumerationObserver(
_ observer: NSFileProviderEnumerationObserver, ncKit: NextcloudKit, numPage: Int,
itemMetadatas: [NextcloudItemMetadataTable]
) {
metadatasToFileProviderItems(itemMetadatas, ncKit: ncKit) { items in
observer.didEnumerate(items)
Logger.enumeration.info("Did enumerate \(items.count) items")
// TODO: Handle paging properly
/*
if items.count == maxItemsPerFileProviderPage {
let nextPage = numPage + 1
let providerPage = NSFileProviderPage("\(nextPage)".data(using: .utf8)!)
observer.finishEnumerating(upTo: providerPage)
} else {
observer.finishEnumerating(upTo: nil)
}
*/
observer.finishEnumerating(upTo: fileProviderPageforNumPage(numPage))
}
}
private static func completeChangesObserver(
_ observer: NSFileProviderChangeObserver, anchor: NSFileProviderSyncAnchor,
ncKit: NextcloudKit,
newMetadatas: [NextcloudItemMetadataTable]?,
updatedMetadatas: [NextcloudItemMetadataTable]?,
deletedMetadatas: [NextcloudItemMetadataTable]?
) {
guard newMetadatas != nil || updatedMetadatas != nil || deletedMetadatas != nil else {
Logger.enumeration.error(
"Received invalid newMetadatas, updatedMetadatas or deletedMetadatas. Finished enumeration of changes with error."
)
observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem))
return
}
// Observer does not care about new vs updated, so join
var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
if let newMetadatas {
allUpdatedMetadatas += newMetadatas
}
if let updatedMetadatas {
allUpdatedMetadatas += updatedMetadatas
}
if let deletedMetadatas {
allDeletedMetadatas = deletedMetadatas
}
let allFpItemDeletionsIdentifiers = Array(
allDeletedMetadatas.map { NSFileProviderItemIdentifier($0.ocId) })
if !allFpItemDeletionsIdentifiers.isEmpty {
observer.didDeleteItems(withIdentifiers: allFpItemDeletionsIdentifiers)
}
metadatasToFileProviderItems(allUpdatedMetadatas, ncKit: ncKit) { updatedItems in
if !updatedItems.isEmpty {
observer.didUpdate(updatedItems)
}
Logger.enumeration.info(
"Processed \(updatedItems.count) new or updated metadatas, \(allDeletedMetadatas.count) deleted metadatas."
)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
}
}
}

View File

@ -16,6 +16,7 @@ import FileProvider
import Foundation
import NCDesktopClientSocketKit
import NextcloudKit
import NextcloudFileProviderKit
import OSLog
extension FileProviderExtension: NSFileProviderServicing {
@ -39,7 +40,8 @@ extension FileProviderExtension: NSFileProviderServicing {
) -> Progress {
Logger.desktopClientConnection.debug("Serving supported service sources")
let clientCommService = ClientCommunicationService(fpExtension: self)
let services = [clientCommService]
let fpuiExtService = FPUIExtensionServiceSource(fpExtension: self)
let services: [NSFileProviderServiceSource] = [clientCommService, fpuiExtService]
completionHandler(services, nil)
let progress = Progress()
progress.cancellationHandler = {
@ -88,18 +90,22 @@ extension FileProviderExtension: NSFileProviderServicing {
}
@objc func setupDomainAccount(user: String, serverUrl: String, password: String) {
let newNcAccount = NextcloudAccount(user: user, serverUrl: serverUrl, password: password)
let newNcAccount = Account(user: user, serverUrl: serverUrl, password: password)
guard newNcAccount != ncAccount else { return }
ncAccount = newNcAccount
ncKit.setup(
user: ncAccount!.username,
userId: ncAccount!.username,
password: ncAccount!.password,
urlBase: ncAccount!.serverUrl,
account: newNcAccount.ncKitAccount,
user: newNcAccount.username,
userId: newNcAccount.username,
password: newNcAccount.password,
urlBase: newNcAccount.serverUrl,
userAgent: "Nextcloud-macOS/FileProviderExt",
nextcloudVersion: 25,
delegate: nil) // TODO: add delegate methods for self
changeObserver = RemoteChangeObserver(ncKit: ncKit, domain: domain)
ncKit.setup(delegate: changeObserver)
Logger.fileProviderExtension.info(
"Nextcloud account set up in File Provider extension for user: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)"
)

View File

@ -15,6 +15,7 @@
import FileProvider
import Foundation
import NextcloudKit
import NextcloudFileProviderKit
import OSLog
extension FileProviderExtension: NSFileProviderThumbnailing {
@ -28,46 +29,12 @@ extension FileProviderExtension: NSFileProviderThumbnailing {
) -> Void,
completionHandler: @escaping (Error?) -> Void
) -> Progress {
let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))
var progressCounter: Int64 = 0
func finishCurrent() {
progressCounter += 1
if progressCounter == progress.totalUnitCount {
completionHandler(nil)
}
}
for itemIdentifier in itemIdentifiers {
Logger.fileProviderExtension.debug(
"Fetching thumbnail for item with identifier:\(itemIdentifier.rawValue, privacy: .public)"
)
guard
let metadata = NextcloudFilesDatabaseManager.shared
.itemMetadataFromFileProviderItemIdentifier(itemIdentifier),
let thumbnailUrl = metadata.thumbnailUrl(size: size)
else {
Logger.fileProviderExtension.debug("Did not fetch thumbnail URL")
finishCurrent()
continue
}
Logger.fileProviderExtension.debug(
"Fetching thumbnail for file:\(metadata.fileName) at:\(thumbnailUrl.absoluteString, privacy: .public)"
)
ncKit.getPreview(url: thumbnailUrl) { _, data, error in
if error == .success, data != nil {
perThumbnailCompletionHandler(itemIdentifier, data, nil)
} else {
perThumbnailCompletionHandler(
itemIdentifier, nil, NSFileProviderError(.serverUnreachable))
}
finishCurrent()
}
}
return progress
return NextcloudFileProviderKit.fetchThumbnails(
for: itemIdentifiers,
requestedSize: size,
usingKit: self.ncKit,
perThumbnailCompletionHandler: perThumbnailCompletionHandler,
completionHandler: completionHandler
)
}
}

View File

@ -15,13 +15,15 @@
import FileProvider
import NCDesktopClientSocketKit
import NextcloudKit
import NextcloudFileProviderKit
import OSLog
@objc class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate {
let domain: NSFileProviderDomain
let ncKit = NextcloudKit()
let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String
var ncAccount: NextcloudAccount?
var ncAccount: Account?
var changeObserver: RemoteChangeObserver?
lazy var ncKitBackground = NKBackground(nkCommonInstance: ncKit.nkCommonInstance)
lazy var socketClient: LocalSocketClient? = {
guard let containerUrl = pathForAppGroupContainer() else {
@ -84,62 +86,25 @@ import OSLog
)
}
// MARK: NSFileProviderReplicatedExtension protocol methods
// MARK: - NSFileProviderReplicatedExtension protocol methods
func item(
for identifier: NSFileProviderItemIdentifier, request _: NSFileProviderRequest,
for identifier: NSFileProviderItemIdentifier,
request _: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void
) -> Progress {
// resolve the given identifier to a record in the model
Logger.fileProviderExtension.debug(
"Received item request for item with identifier: \(identifier.rawValue, privacy: .public)"
)
if identifier == .rootContainer {
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not providing item: \(identifier.rawValue, privacy: .public) as account not set up yet"
)
completionHandler(nil, NSFileProviderError(.notAuthenticated))
return Progress()
}
let metadata = NextcloudItemMetadataTable()
metadata.account = ncAccount.ncKitAccount
metadata.directory = true
metadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
metadata.fileName = "root"
metadata.fileNameView = "root"
metadata.serverUrl = ncAccount.serverUrl
metadata.classFile = NKCommon.TypeClassFile.directory.rawValue
completionHandler(
FileProviderItem(
metadata: metadata,
parentItemIdentifier: NSFileProviderItemIdentifier.rootContainer,
ncKit: ncKit), nil)
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
guard let metadata = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier),
let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(metadata)
else {
if let item = Item.storedItem(identifier: identifier, usingKit: ncKit) {
completionHandler(item, nil)
} else {
completionHandler(nil, NSFileProviderError(.noSuchItem))
return Progress()
}
completionHandler(
FileProviderItem(
metadata: metadata, parentItemIdentifier: parentItemIdentifier, ncKit: ncKit), nil)
return Progress()
}
func fetchContents(
for itemIdentifier: NSFileProviderItemIdentifier,
version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest,
version requestedVersion: NSFileProviderItemVersion?,
request: NSFileProviderRequest,
completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void
) -> Progress {
Logger.fileProviderExtension.debug(
@ -149,648 +114,206 @@ import OSLog
guard requestedVersion == nil else {
// TODO: Add proper support for file versioning
Logger.fileProviderExtension.error(
"Can't return contents for specific version as this is not supported.")
"Can't return contents for a specific version as this is not supported."
)
completionHandler(
nil, nil,
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo: [:]))
nil,
nil,
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError)
)
return Progress()
}
guard ncAccount != nil else {
Logger.fileProviderExtension.error(
"Not fetching contents item: \(itemIdentifier.rawValue, privacy: .public) as account not set up yet"
"""
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
as account not set up yet.
"""
)
completionHandler(nil, nil, NSFileProviderError(.notAuthenticated))
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
let ocId = itemIdentifier.rawValue
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
guard let item = Item.storedItem(identifier: itemIdentifier, usingKit: ncKit) else {
Logger.fileProviderExtension.error(
"Could not acquire metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public)"
"""
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
as item not found.
"""
)
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return Progress()
}
guard !metadata.isDocumentViewableOnly else {
Logger.fileProviderExtension.error(
"Could not get contents of item as is readonly: \(itemIdentifier.rawValue, privacy: .public) \(metadata.fileName, privacy: .public)"
)
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
return Progress()
}
let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
Logger.fileProviderExtension.debug(
"Fetching file with name \(metadata.fileName, privacy: .public) at URL: \(serverUrlFileName, privacy: .public)"
)
let progress = Progress()
// TODO: Handle folders nicely
do {
let fileNameLocalPath = try localPathForNCFile(
ocId: metadata.ocId, fileNameView: metadata.fileNameView, domain: domain)
dbManager.setStatusForItemMetadata(
metadata, status: NextcloudItemMetadataTable.Status.downloading
) { updatedMetadata in
guard let updatedMetadata else {
Logger.fileProviderExtension.error(
"Could not acquire updated metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public), unable to update item status to downloading"
)
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return
}
self.ncKit.download(
serverUrlFileName: serverUrlFileName,
fileNameLocalPath: fileNameLocalPath.path,
requestHandler: { request in
progress.setHandlersFromAfRequest(request)
},
taskHandler: { task in
NSFileProviderManager(for: self.domain)?.register(
task, forItemWithIdentifier: itemIdentifier, completionHandler: { _ in }
)
},
progressHandler: { downloadProgress in
downloadProgress.copyCurrentStateToProgress(progress)
}
) { _, etag, date, _, _, _, error in
if error == .success {
Logger.fileTransfer.debug(
"Acquired contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public) and filename: \(updatedMetadata.fileName, privacy: .public)"
)
updatedMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
updatedMetadata.sessionError = ""
updatedMetadata.date = (date ?? NSDate()) as Date
updatedMetadata.etag = etag ?? ""
dbManager.addLocalFileMetadataFromItemMetadata(updatedMetadata)
dbManager.addItemMetadata(updatedMetadata)
guard
let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(
updatedMetadata)
else {
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return
}
let fpItem = FileProviderItem(
metadata: updatedMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
completionHandler(fileNameLocalPath, fpItem, nil)
} else {
Logger.fileTransfer.error(
"Could not acquire contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public) and fileName: \(updatedMetadata.fileName, privacy: .public)"
)
updatedMetadata.status =
NextcloudItemMetadataTable.Status.downloadError.rawValue
updatedMetadata.sessionError = error.errorDescription
dbManager.addItemMetadata(updatedMetadata)
completionHandler(nil, nil, error.fileProviderError)
}
}
}
} catch {
Logger.fileProviderExtension.error(
"Could not find local path for file \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
Task {
let (localUrl, updatedItem, error) = await item.fetchContents(
domain: self.domain, progress: progress
)
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
completionHandler(localUrl, updatedItem, error)
}
return progress
}
func createItem(
basedOn itemTemplate: NSFileProviderItem, fields _: NSFileProviderItemFields,
contents url: URL?, options: NSFileProviderCreateItemOptions = [],
basedOn itemTemplate: NSFileProviderItem,
fields: NSFileProviderItemFields,
contents url: URL?,
options: NSFileProviderCreateItemOptions = [],
request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?)
->
Void
completionHandler: @escaping (
NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?
) -> Void
) -> Progress {
// TODO: a new item was created on disk, process the item's creation
let tempId = itemTemplate.itemIdentifier.rawValue
Logger.fileProviderExtension.debug(
"Received create item request for item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)"
"""
Received create item request for item with identifier: \(tempId, privacy: .public)
and filename: \(itemTemplate.filename, privacy: .public)
"""
)
guard itemTemplate.contentType != .symbolicLink else {
Logger.fileProviderExtension.error("Cannot create item, symbolic links not supported.")
completionHandler(
itemTemplate, NSFileProviderItemFields(), false,
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo: [:]))
return Progress()
}
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as account not set up yet"
"""
Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public)
as account not set up yet
"""
)
completionHandler(
itemTemplate, NSFileProviderItemFields(), false,
NSFileProviderError(.notAuthenticated))
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
let parentItemIdentifier = itemTemplate.parentItemIdentifier
let itemTemplateIsFolder =
itemTemplate.contentType == .folder || itemTemplate.contentType == .directory
if options.contains(.mayAlreadyExist) {
// TODO: This needs to be properly handled with a check in the db
Logger.fileProviderExtension.info(
"Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as it may already exist"
itemTemplate,
NSFileProviderItemFields(),
false,
NSFileProviderError(.notAuthenticated)
)
completionHandler(
itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
return Progress()
}
var parentItemServerUrl: String
if parentItemIdentifier == .rootContainer {
parentItemServerUrl = ncAccount.davFilesUrl
} else {
guard
let parentItemMetadata = dbManager.directoryMetadata(
ocId: parentItemIdentifier.rawValue)
else {
Logger.fileProviderExtension.error(
"Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public), could not find metadata for parentItemIdentifier \(parentItemIdentifier.rawValue, privacy: .public)"
)
completionHandler(
itemTemplate, NSFileProviderItemFields(), false,
NSFileProviderError(.noSuchItem))
return Progress()
}
parentItemServerUrl = parentItemMetadata.serverUrl + "/" + parentItemMetadata.fileName
}
let fileNameLocalPath = url?.path ?? ""
let newServerUrlFileName = parentItemServerUrl + "/" + itemTemplate.filename
Logger.fileProviderExtension.debug(
"About to upload item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) of type: \(itemTemplate.contentType?.identifier ?? "UNKNOWN") (is folder: \(itemTemplateIsFolder ? "yes" : "no") and filename: \(itemTemplate.filename) to server url: \(newServerUrlFileName, privacy: .public) with contents located at: \(fileNameLocalPath, privacy: .public)"
)
if itemTemplateIsFolder {
ncKit.createFolder(serverUrlFileName: newServerUrlFileName) { account, _, _, error in
guard error == .success else {
Logger.fileTransfer.error(
"Could not create new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(itemTemplate, [], false, error.fileProviderError)
return
}
// Read contents after creation
self.ncKit.readFileOrFolder(
serverUrlFileName: newServerUrlFileName, depth: "0", showHiddenFiles: true
) { account, files, _, error in
guard error == .success else {
Logger.fileTransfer.error(
"Could not read new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
return
}
DispatchQueue.global().async {
NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(
files, account: account
) {
directoryMetadata, _, _ in
dbManager.addItemMetadata(directoryMetadata)
let fpItem = FileProviderItem(
metadata: directoryMetadata,
parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
completionHandler(fpItem, [], true, nil)
}
}
}
}
return Progress()
}
let progress = Progress()
ncKit.upload(
serverUrlFileName: newServerUrlFileName,
fileNameLocalPath: fileNameLocalPath,
requestHandler: { request in
progress.setHandlersFromAfRequest(request)
},
taskHandler: { task in
NSFileProviderManager(for: self.domain)?.register(
task, forItemWithIdentifier: itemTemplate.itemIdentifier,
completionHandler: { _ in })
},
progressHandler: { uploadProgress in
uploadProgress.copyCurrentStateToProgress(progress)
}
) { account, ocId, etag, date, size, _, _, error in
guard error == .success, let ocId else {
Logger.fileTransfer.error(
"Could not upload item with filename: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(itemTemplate, [], false, error.fileProviderError)
return
}
Logger.fileTransfer.info(
"Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)"
Task {
let (item, error) = await Item.create(
basedOn: itemTemplate,
fields: fields,
contents: url,
request: request,
domain: self.domain,
ncKit: ncKit,
ncAccount: ncAccount,
progress: progress
)
if size != itemTemplate.documentSize as? Int64 {
Logger.fileTransfer.warning(
"Created item upload reported as successful, but there are differences between the received file size (\(size, privacy: .public)) and the original file size (\(itemTemplate.documentSize??.int64Value ?? 0))"
)
if error != nil {
signalEnumerator(completionHandler: { _ in })
}
let newMetadata = NextcloudItemMetadataTable()
newMetadata.date = (date ?? NSDate()) as Date
newMetadata.etag = etag ?? ""
newMetadata.account = account
newMetadata.fileName = itemTemplate.filename
newMetadata.fileNameView = itemTemplate.filename
newMetadata.ocId = ocId
newMetadata.size = size
newMetadata.contentType = itemTemplate.contentType?.preferredMIMEType ?? ""
newMetadata.directory = itemTemplateIsFolder
newMetadata.serverUrl = parentItemServerUrl
newMetadata.session = ""
newMetadata.sessionError = ""
newMetadata.sessionTaskIdentifier = 0
newMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
dbManager.addLocalFileMetadataFromItemMetadata(newMetadata)
dbManager.addItemMetadata(newMetadata)
let fpItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit
completionHandler(
item ?? itemTemplate,
NSFileProviderItemFields(),
false,
error
)
completionHandler(fpItem, [], false, nil)
}
return progress
}
func modifyItem(
_ item: NSFileProviderItem, baseVersion _: NSFileProviderItemVersion,
changedFields: NSFileProviderItemFields, contents newContents: URL?,
options: NSFileProviderModifyItemOptions = [], request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?)
->
Void
_ item: NSFileProviderItem,
baseVersion: NSFileProviderItemVersion,
changedFields: NSFileProviderItemFields,
contents newContents: URL?,
options: NSFileProviderModifyItemOptions = [],
request: NSFileProviderRequest,
completionHandler: @escaping (
NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?
) -> Void
) -> Progress {
// An item was modified on disk, process the item's modification
// TODO: Handle finder things like tags, other possible item changed fields
let identifier = item.itemIdentifier
let ocId = identifier.rawValue
Logger.fileProviderExtension.debug(
"Received modify item request for item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) and filename: \(item.filename, privacy: .public)"
"""
Received modify item request for item with identifier: \(ocId, privacy: .public)
and filename: \(item.filename, privacy: .public)
"""
)
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public) as account not set up yet"
"Not modifying item: \(ocId, privacy: .public) as account not set up yet."
)
completionHandler(item, [], false, NSFileProviderError(.notAuthenticated))
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
let parentItemIdentifier = item.parentItemIdentifier
let itemTemplateIsFolder = item.contentType == .folder || item.contentType == .directory
if options.contains(.mayAlreadyExist) {
// TODO: This needs to be properly handled with a check in the db
Logger.fileProviderExtension.warning(
"Modification for item: \(item.itemIdentifier.rawValue, privacy: .public) may already exist"
guard let existingItem = Item.storedItem(identifier: identifier, usingKit: ncKit) else {
Logger.fileProviderExtension.error(
"Not modifying item: \(ocId, privacy: .public) as item not found."
)
}
var parentItemServerUrl: String
if parentItemIdentifier == .rootContainer {
parentItemServerUrl = ncAccount.davFilesUrl
} else {
guard
let parentItemMetadata = dbManager.directoryMetadata(
ocId: parentItemIdentifier.rawValue)
else {
Logger.fileProviderExtension.error(
"Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public), could not find metadata for parentItemIdentifier \(parentItemIdentifier.rawValue, privacy: .public)"
)
completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
return Progress()
}
parentItemServerUrl = parentItemMetadata.serverUrl + "/" + parentItemMetadata.fileName
}
let fileNameLocalPath = newContents?.path ?? ""
let newServerUrlFileName = parentItemServerUrl + "/" + item.filename
Logger.fileProviderExtension.debug(
"About to upload modified item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) of type: \(item.contentType?.identifier ?? "UNKNOWN") (is folder: \(itemTemplateIsFolder ? "yes" : "no") and filename: \(item.filename, privacy: .public) to server url: \(newServerUrlFileName, privacy: .public) with contents located at: \(fileNameLocalPath, privacy: .public)"
)
var modifiedItem = item
// Create a serial dispatch queue
// We want to wait for network operations to finish before we fire off subsequent network
// operations, or we might cause explosions (e.g. trying to modify items that have just been
// moved elsewhere)
let dispatchQueue = DispatchQueue(label: "modifyItemQueue", qos: .userInitiated)
if changedFields.contains(.filename) || changedFields.contains(.parentItemIdentifier) {
dispatchQueue.async {
let ocId = item.itemIdentifier.rawValue
Logger.fileProviderExtension.debug(
"Changed fields for item \(ocId, privacy: .public) with filename \(item.filename, privacy: .public) includes filename or parentitemidentifier..."
)
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileProviderExtension.error(
"Could not acquire metadata of item with identifier: \(item.itemIdentifier.rawValue, privacy: .public)"
)
completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
return
}
var renameError: NSFileProviderError?
let oldServerUrlFileName = metadata.serverUrl + "/" + metadata.fileName
let moveFileOrFolderDispatchGroup = DispatchGroup() // Make this block wait until done
moveFileOrFolderDispatchGroup.enter()
self.ncKit.moveFileOrFolder(
serverUrlFileNameSource: oldServerUrlFileName,
serverUrlFileNameDestination: newServerUrlFileName,
overwrite: false
) { _, error in
guard error == .success else {
Logger.fileTransfer.error(
"Could not move file or folder: \(oldServerUrlFileName, privacy: .public) to \(newServerUrlFileName, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
renameError = error.fileProviderError
moveFileOrFolderDispatchGroup.leave()
return
}
// Remember that a folder metadata's serverUrl is its direct server URL, while for
// an item metadata the server URL is the parent folder's URL
if itemTemplateIsFolder {
_ = dbManager.renameDirectoryAndPropagateToChildren(
ocId: ocId, newServerUrl: newServerUrlFileName,
newFileName: item.filename)
self.signalEnumerator { error in
if error != nil {
Logger.fileTransfer.error(
"Error notifying change in moved directory: \(error)")
}
}
} else {
dbManager.renameItemMetadata(
ocId: ocId, newServerUrl: parentItemServerUrl,
newFileName: item.filename)
}
guard let newMetadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileTransfer.error(
"Could not acquire metadata of item with identifier: \(ocId, privacy: .public), cannot correctly inform of modification"
)
renameError = NSFileProviderError(.noSuchItem)
moveFileOrFolderDispatchGroup.leave()
return
}
modifiedItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
moveFileOrFolderDispatchGroup.leave()
}
moveFileOrFolderDispatchGroup.wait()
guard renameError == nil else {
Logger.fileTransfer.error(
"Stopping rename of item with ocId \(ocId, privacy: .public) due to error: \(renameError!.localizedDescription, privacy: .public)"
)
completionHandler(modifiedItem, [], false, renameError)
return
}
guard !itemTemplateIsFolder else {
Logger.fileTransfer.debug(
"Only handling renaming for folders. ocId: \(ocId, privacy: .public)")
completionHandler(modifiedItem, [], false, nil)
return
}
}
// Return the progress if item is folder here while the async block runs
guard !itemTemplateIsFolder else {
return Progress()
}
}
guard !itemTemplateIsFolder else {
Logger.fileTransfer.debug(
"System requested modification for folder with ocID \(item.itemIdentifier.rawValue, privacy: .public) (\(newServerUrlFileName, privacy: .public)) of something other than folder name."
)
completionHandler(modifiedItem, [], false, nil)
completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
return Progress()
}
let progress = Progress()
if changedFields.contains(.contents) {
dispatchQueue.async {
Logger.fileProviderExtension.debug(
"Item modification for \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public) includes contents"
)
guard newContents != nil else {
Logger.fileProviderExtension.warning(
"WARNING. Could not upload modified contents as was provided nil contents url. ocId: \(item.itemIdentifier.rawValue, privacy: .public)"
)
completionHandler(modifiedItem, [], false, NSFileProviderError(.noSuchItem))
return
}
let ocId = item.itemIdentifier.rawValue
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileProviderExtension.error(
"Could not acquire metadata of item with identifier: \(ocId, privacy: .public)"
)
completionHandler(
item, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
return
}
dbManager.setStatusForItemMetadata(
metadata, status: NextcloudItemMetadataTable.Status.uploading
) { updatedMetadata in
if updatedMetadata == nil {
Logger.fileProviderExtension.warning(
"Could not acquire updated metadata of item with identifier: \(ocId, privacy: .public), unable to update item status to uploading"
)
}
self.ncKit.upload(
serverUrlFileName: newServerUrlFileName,
fileNameLocalPath: fileNameLocalPath,
requestHandler: { request in
progress.setHandlersFromAfRequest(request)
},
taskHandler: { task in
NSFileProviderManager(for: self.domain)?.register(
task, forItemWithIdentifier: item.itemIdentifier,
completionHandler: { _ in })
},
progressHandler: { uploadProgress in
uploadProgress.copyCurrentStateToProgress(progress)
}
) { account, ocId, etag, date, size, _, _, error in
if error == .success, let ocId {
Logger.fileProviderExtension.info(
"Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(item.filename, privacy: .public)"
)
if size != item.documentSize as? Int64 {
Logger.fileTransfer.warning(
"Created item upload reported as successful, but there are differences between the received file size (\(size, privacy: .public)) and the original file size (\(item.documentSize??.int64Value ?? 0))"
)
}
let newMetadata = NextcloudItemMetadataTable()
newMetadata.date = (date ?? NSDate()) as Date
newMetadata.etag = etag ?? ""
newMetadata.account = account
newMetadata.fileName = item.filename
newMetadata.fileNameView = item.filename
newMetadata.ocId = ocId
newMetadata.size = size
newMetadata.contentType = item.contentType?.preferredMIMEType ?? ""
newMetadata.directory = itemTemplateIsFolder
newMetadata.serverUrl = parentItemServerUrl
newMetadata.session = ""
newMetadata.sessionError = ""
newMetadata.sessionTaskIdentifier = 0
newMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
dbManager.addLocalFileMetadataFromItemMetadata(newMetadata)
dbManager.addItemMetadata(newMetadata)
modifiedItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit
)
completionHandler(modifiedItem, [], false, nil)
} else {
Logger.fileTransfer.error(
"Could not upload item \(item.itemIdentifier.rawValue, privacy: .public) with filename: \(item.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
metadata.status = NextcloudItemMetadataTable.Status.uploadError.rawValue
metadata.sessionError = error.errorDescription
dbManager.addItemMetadata(metadata)
completionHandler(modifiedItem, [], false, error.fileProviderError)
return
}
}
}
}
} else {
Logger.fileProviderExtension.debug(
"Nothing more to do with \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public), modifications complete"
Task {
let (modifiedItem, error) = await existingItem.modify(
itemTarget: item,
baseVersion: baseVersion,
changedFields: changedFields,
contents: newContents,
options: options,
request: request,
ncAccount: ncAccount,
domain: domain,
progress: progress
)
completionHandler(modifiedItem, [], false, nil)
if error != nil {
signalEnumerator(completionHandler: { _ in })
}
completionHandler(modifiedItem ?? item, [], false, error)
}
return progress
}
func deleteItem(
identifier: NSFileProviderItemIdentifier, baseVersion _: NSFileProviderItemVersion,
options _: NSFileProviderDeleteItemOptions = [], request _: NSFileProviderRequest,
identifier: NSFileProviderItemIdentifier,
baseVersion _: NSFileProviderItemVersion,
options _: NSFileProviderDeleteItemOptions = [],
request _: NSFileProviderRequest,
completionHandler: @escaping (Error?) -> Void
) -> Progress {
Logger.fileProviderExtension.debug(
"Received delete item request for item with identifier: \(identifier.rawValue, privacy: .public)"
"Received delete request for item: \(identifier.rawValue, privacy: .public)"
)
guard ncAccount != nil else {
Logger.fileProviderExtension.error(
"Not deleting item: \(identifier.rawValue, privacy: .public) as account not set up yet"
"Not deleting item \(identifier.rawValue, privacy: .public), account not set up yet"
)
completionHandler(NSFileProviderError(.notAuthenticated))
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
let ocId = identifier.rawValue
guard let itemMetadata = dbManager.itemMetadataFromOcId(ocId) else {
completionHandler(NSFileProviderError(.noSuchItem))
return Progress()
}
let serverFileNameUrl = itemMetadata.serverUrl + "/" + itemMetadata.fileName
guard serverFileNameUrl != "" else {
completionHandler(NSFileProviderError(.noSuchItem))
return Progress()
}
ncKit.deleteFileOrFolder(serverUrlFileName: serverFileNameUrl) { _, error in
guard error == .success else {
Logger.fileTransfer.error(
"Could not delete item with ocId \(identifier.rawValue, privacy: .public) at \(serverFileNameUrl, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(error.fileProviderError)
return
}
Logger.fileTransfer.info(
"Successfully deleted item with identifier: \(identifier.rawValue, privacy: .public) at: \(serverFileNameUrl, privacy: .public)"
guard let item = Item.storedItem(identifier: identifier, usingKit: ncKit) else {
Logger.fileProviderExtension.error(
"Not deleting item \(identifier.rawValue, privacy: .public), item not found"
)
if itemMetadata.directory {
_ = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: ocId)
} else {
dbManager.deleteItemMetadata(ocId: ocId)
if dbManager.localFileMetadataFromOcId(ocId) != nil {
dbManager.deleteLocalFileMetadata(ocId: ocId)
}
}
completionHandler(nil)
completionHandler(NSFileProviderError(.noSuchItem))
return Progress()
}
return Progress()
let progress = Progress(totalUnitCount: 1)
Task {
let error = await item.delete()
if error != nil {
signalEnumerator(completionHandler: { _ in })
}
progress.completedUnitCount = 1
completionHandler(await item.delete())
}
return progress
}
func enumerator(
@ -803,10 +326,11 @@ import OSLog
throw NSFileProviderError(.notAuthenticated)
}
return FileProviderEnumerator(
return Enumerator(
enumeratedItemIdentifier: containerItemIdentifier,
ncAccount: ncAccount,
ncKit: ncKit,
domain: domain,
fastEnumeration: config.fastEnumerationEnabled
)
}
@ -828,7 +352,7 @@ import OSLog
}
let materialisedEnumerator = fpManager.enumeratorForMaterializedItems()
let materialisedObserver = FileProviderMaterialisedEnumerationObserver(
let materialisedObserver = MaterialisedEnumerationObserver(
ncKitAccount: ncAccount.ncKitAccount
) { _ in
completionHandler()

View File

@ -1,164 +0,0 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import NextcloudKit
import UniformTypeIdentifiers
class FileProviderItem: NSObject, NSFileProviderItem {
enum FileProviderItemTransferError: Error {
case downloadError
case uploadError
}
let metadata: NextcloudItemMetadataTable
let parentItemIdentifier: NSFileProviderItemIdentifier
let ncKit: NextcloudKit
var itemIdentifier: NSFileProviderItemIdentifier {
NSFileProviderItemIdentifier(metadata.ocId)
}
var capabilities: NSFileProviderItemCapabilities {
guard !metadata.directory else {
if #available(macOS 13.0, *) {
// .allowsEvicting deprecated on macOS 13.0+, use contentPolicy instead
return [
.allowsAddingSubItems,
.allowsContentEnumerating,
.allowsReading,
.allowsDeleting,
.allowsRenaming
]
} else {
return [
.allowsAddingSubItems,
.allowsContentEnumerating,
.allowsReading,
.allowsDeleting,
.allowsRenaming,
.allowsEvicting
]
}
}
guard !metadata.lock else {
return [.allowsReading]
}
return [
.allowsWriting,
.allowsReading,
.allowsDeleting,
.allowsRenaming,
.allowsReparenting,
.allowsEvicting
]
}
var itemVersion: NSFileProviderItemVersion {
NSFileProviderItemVersion(
contentVersion: metadata.etag.data(using: .utf8)!,
metadataVersion: metadata.etag.data(using: .utf8)!)
}
var filename: String {
metadata.fileNameView
}
var contentType: UTType {
if itemIdentifier == .rootContainer || metadata.directory {
return .folder
}
let internalType = ncKit.nkCommonInstance.getInternalType(
fileName: metadata.fileNameView,
mimeType: "",
directory: metadata.directory)
return UTType(filenameExtension: internalType.ext) ?? .content
}
var documentSize: NSNumber? {
NSNumber(value: metadata.size)
}
var creationDate: Date? {
metadata.creationDate as Date
}
var lastUsedDate: Date? {
metadata.date as Date
}
var contentModificationDate: Date? {
metadata.date as Date
}
var isDownloaded: Bool {
metadata.directory
|| NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
}
var isDownloading: Bool {
metadata.status == NextcloudItemMetadataTable.Status.downloading.rawValue
}
var downloadingError: Error? {
if metadata.status == NextcloudItemMetadataTable.Status.downloadError.rawValue {
return FileProviderItemTransferError.downloadError
}
return nil
}
var isUploaded: Bool {
NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
}
var isUploading: Bool {
metadata.status == NextcloudItemMetadataTable.Status.uploading.rawValue
}
var uploadingError: Error? {
if metadata.status == NextcloudItemMetadataTable.Status.uploadError.rawValue {
FileProviderItemTransferError.uploadError
} else {
nil
}
}
var childItemCount: NSNumber? {
if metadata.directory {
NSNumber(
integerLiteral: NextcloudFilesDatabaseManager.shared.childItemsForDirectory(
metadata
).count)
} else {
nil
}
}
@available(macOSApplicationExtension 13.0, *)
var contentPolicy: NSFileProviderContentPolicy {
.downloadLazily
}
required init(
metadata: NextcloudItemMetadataTable,
parentItemIdentifier: NSFileProviderItemIdentifier,
ncKit: NextcloudKit
) {
self.metadata = metadata
self.parentItemIdentifier = parentItemIdentifier
self.ncKit = ncKit
super.init()
}
}

View File

@ -14,6 +14,7 @@
import FileProvider
import Foundation
import NextcloudFileProviderKit
import OSLog
class FileProviderMaterialisedEnumerationObserver: NSObject, NSFileProviderEnumerationObserver {
@ -59,7 +60,7 @@ class FileProviderMaterialisedEnumerationObserver: NSObject, NSFileProviderEnume
_ itemIds: Set<String>, account: String,
completionHandler: @escaping (_ deletedOcIds: Set<String>) -> Void
) {
let dbManager = NextcloudFilesDatabaseManager.shared
let dbManager = FilesDatabaseManager.shared
let databaseLocalFileMetadatas = dbManager.localFileMetadatas(account: account)
var noLongerMaterialisedIds = Set<String>()

View File

@ -1,68 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
import OSLog
func pathForAppGroupContainer() -> URL? {
guard
let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix")
as? String
else {
Logger.localFileOps.critical(
"Could not get container url as missing SocketApiPrefix info in app Info.plist")
return nil
}
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupIdentifier)
}
func pathForFileProviderExtData() -> URL? {
let containerUrl = pathForAppGroupContainer()
return containerUrl?.appendingPathComponent("FileProviderExt/")
}
func pathForFileProviderTempFilesForDomain(_ domain: NSFileProviderDomain) throws -> URL? {
guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.localFileOps.error(
"Unable to get file provider manager for domain: \(domain.displayName, privacy: .public)"
)
throw NSFileProviderError(.providerNotFound)
}
let fileProviderDataUrl = try fpManager.temporaryDirectoryURL()
return fileProviderDataUrl.appendingPathComponent("TemporaryNextcloudFiles/")
}
func localPathForNCFile(ocId _: String, fileNameView: String, domain: NSFileProviderDomain) throws
-> URL
{
guard let fileProviderFilesPathUrl = try pathForFileProviderTempFilesForDomain(domain) else {
Logger.localFileOps.error(
"Unable to get path for file provider temp files for domain: \(domain.displayName, privacy: .public)"
)
throw URLError(.badURL)
}
let filePathUrl = fileProviderFilesPathUrl.appendingPathComponent(fileNameView)
let filePath = filePathUrl.path
if !FileManager.default.fileExists(atPath: filePath) {
FileManager.default.createFile(atPath: filePath, contents: nil)
}
return filePathUrl
}

View File

@ -1,29 +0,0 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
struct NextcloudAccount: Equatable {
static let webDavFilesUrlSuffix: String = "/remote.php/dav/files/"
let username, password, ncKitAccount, serverUrl, davFilesUrl: String
init(user: String, serverUrl: String, password: String) {
username = user
self.password = password
ncKitAccount = user + " " + serverUrl
self.serverUrl = serverUrl
davFilesUrl = serverUrl + NextcloudAccount.webDavFilesUrlSuffix + user
}
}

View File

@ -0,0 +1,18 @@
//
// FPUIExtensionCommunicationProtocol.swift
// FileProviderExt
//
// Created by Claudio Cambra on 21/2/24.
//
import FileProvider
import NextcloudKit
let fpUiExtensionServiceName = NSFileProviderServiceName(
"com.nextcloud.desktopclient.FPUIExtensionService"
)
@objc protocol FPUIExtensionService {
func credentials() async -> NSDictionary
func itemServerPath(identifier: NSFileProviderItemIdentifier) async -> NSString?
}

View File

@ -0,0 +1,65 @@
//
// FPUIExtensionCommunicationService.swift
// FileProviderExt
//
// Created by Claudio Cambra on 21/2/24.
//
import FileProvider
import Foundation
import NextcloudKit
import NextcloudFileProviderKit
import OSLog
class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, FPUIExtensionService {
let listener = NSXPCListener.anonymous()
let serviceName = fpUiExtensionServiceName
let fpExtension: FileProviderExtension
init(fpExtension: FileProviderExtension) {
Logger.fpUiExtensionService.debug("Instantiating FPUIExtensionService service")
self.fpExtension = fpExtension
super.init()
}
func makeListenerEndpoint() throws -> NSXPCListenerEndpoint {
listener.delegate = self
listener.resume()
return listener.endpoint
}
func listener(
_ listener: NSXPCListener,
shouldAcceptNewConnection newConnection: NSXPCConnection
) -> Bool {
newConnection.exportedInterface = NSXPCInterface(with: FPUIExtensionService.self)
newConnection.exportedObject = self
newConnection.resume()
return true
}
//MARK: - FPUIExtensionService protocol methods
func credentials() async -> NSDictionary {
return (fpExtension.ncAccount?.dictionary() ?? [:]) as NSDictionary
}
func itemServerPath(identifier: NSFileProviderItemIdentifier) async -> NSString? {
let rawIdentifier = identifier.rawValue
Logger.shares.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
guard let baseUrl = fpExtension.ncAccount?.davFilesUrl else {
Logger.shares.error("Could not fetch shares as ncAccount on parent extension is nil")
return nil
}
let dbManager = FilesDatabaseManager.shared
guard let item = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier) else {
Logger.shares.error("No item \(rawIdentifier, privacy: .public) in db, no shares.")
return nil
}
let completePath = item.serverUrl + "/" + item.fileName
return completePath.replacingOccurrences(of: baseUrl, with: "") as NSString
}
}

View File

@ -0,0 +1,57 @@
//
// DocumentActionViewController.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 20/2/24.
//
import FileProviderUI
import OSLog
class DocumentActionViewController: FPUIActionExtensionViewController {
var domain: NSFileProviderDomain {
guard let identifier = extensionContext.domainIdentifier else {
fatalError("not expected to be called with default domain")
}
return NSFileProviderDomain(
identifier: NSFileProviderDomainIdentifier(rawValue: identifier.rawValue),
displayName: ""
)
}
func prepare(childViewController: NSViewController) {
addChild(childViewController)
view.addSubview(childViewController.view)
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: childViewController.view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: childViewController.view.trailingAnchor),
view.topAnchor.constraint(equalTo: childViewController.view.topAnchor),
view.bottomAnchor.constraint(equalTo: childViewController.view.bottomAnchor)
])
}
override func prepare(
forAction actionIdentifier: String, itemIdentifiers: [NSFileProviderItemIdentifier]
) {
Logger.actionViewController.info("Preparing action: \(actionIdentifier, privacy: .public)")
if actionIdentifier == "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction" {
prepare(childViewController: ShareViewController(itemIdentifiers))
}
}
override func prepare(forError error: Error) {
Logger.actionViewController.info(
"""
Preparing for error: \(error.localizedDescription, privacy: .public)
"""
)
}
override public func loadView() {
self.view = NSView()
}
}

View File

@ -0,0 +1,21 @@
//
// Logger+Extensions.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 21/2/24.
//
import OSLog
extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier!
static let actionViewController = Logger(subsystem: subsystem, category: "actionViewController")
static let shareCapabilities = Logger(subsystem: subsystem, category: "shareCapabilities")
static let shareController = Logger(subsystem: subsystem, category: "shareController")
static let shareeDataSource = Logger(subsystem: subsystem, category: "shareeDataSource")
static let sharesDataSource = Logger(subsystem: subsystem, category: "sharesDataSource")
static let shareOptionsView = Logger(subsystem: subsystem, category: "shareOptionsView")
static let shareViewController = Logger(subsystem: subsystem, category: "shareViewController")
}

View File

@ -0,0 +1,130 @@
//
// NKShare+Extensions.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 28/2/24.
//
import AppKit
import NextcloudKit
extension NKShare {
enum ShareType: Int {
case user = 0
case group = 1
case publicLink = 3
case email = 4
case federatedCloud = 6
case circle = 7
case talkConversation = 10
}
enum PermissionValues: Int {
case readShare = 1
case updateShare = 2
case createShare = 4
case deleteShare = 8
case shareShare = 16
case all = 31
}
var typeImage: NSImage? {
var image: NSImage?
switch shareType {
case ShareType.user.rawValue:
image = NSImage(
systemSymbolName: "person.circle.fill",
accessibilityDescription: "User share icon"
)
case ShareType.group.rawValue:
image = NSImage(
systemSymbolName: "person.2.circle.fill",
accessibilityDescription: "Group share icon"
)
case ShareType.publicLink.rawValue:
image = NSImage(
systemSymbolName: "link.circle.fill",
accessibilityDescription: "Public link share icon"
)
case ShareType.email.rawValue:
image = NSImage(
systemSymbolName: "envelope.circle.fill",
accessibilityDescription: "Email share icon"
)
case ShareType.federatedCloud.rawValue:
image = NSImage(
systemSymbolName: "cloud.circle.fill",
accessibilityDescription: "Federated cloud share icon"
)
case ShareType.circle.rawValue:
image = NSImage(
systemSymbolName: "circle.circle.fill",
accessibilityDescription: "Circle share icon"
)
case ShareType.talkConversation.rawValue:
image = NSImage(
systemSymbolName: "message.circle.fill",
accessibilityDescription: "Talk conversation share icon"
)
default:
return nil
}
var config = NSImage.SymbolConfiguration(textStyle: .body, scale: .large)
if #available(macOS 12.0, *) {
config = config.applying(
.init(paletteColors: [.controlBackgroundColor, .controlAccentColor])
)
}
return image?.withSymbolConfiguration(config)
}
var displayString: String {
if label != "" {
return label
}
switch shareType {
case ShareType.user.rawValue:
return "User share (\(shareWith))"
case ShareType.group.rawValue:
return "Group share (\(shareWith))"
case ShareType.publicLink.rawValue:
return "Public link share"
case ShareType.email.rawValue:
return "Email share (\(shareWith))"
case ShareType.federatedCloud.rawValue:
return "Federated cloud share (\(shareWith))"
case ShareType.circle.rawValue:
return "Circle share (\(shareWith))"
case ShareType.talkConversation.rawValue:
return "Talk conversation share (\(shareWith))"
default:
return "Unknown share"
}
}
var expirationDateString: String? {
guard let date = expirationDate else { return nil }
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss"
return dateFormatter.string(from: date as Date)
}
var shareesCanEdit: Bool {
get { (permissions & PermissionValues.updateShare.rawValue) != 0 }
set {
if newValue {
permissions |= NKShare.PermissionValues.updateShare.rawValue
} else {
permissions &= ~NKShare.PermissionValues.updateShare.rawValue
}
}
}
static func formattedDateString(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss"
return dateFormatter.string(from: date)
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN)</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleDisplayName</key>
<string>$(OC_APPLICATION_NAME) File Provider UI Extension</string>
<key>CFBundleIdentifier</key>
<string>$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionFileProviderActions</key>
<array>
<dict>
<key>NSExtensionFileProviderActionActivationRule</key>
<string>TRUEPREDICATE</string>
<key>NSExtensionFileProviderActionIdentifier</key>
<string>com.nextcloud.desktopclient.FileProviderUIExt.ShareAction</string>
<key>NSExtensionFileProviderActionName</key>
<string>Share options</string>
</dict>
</array>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.fileprovider-actionsui</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).DocumentActionViewController</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,155 @@
//
// ShareController.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 4/3/24.
//
import Combine
import Foundation
import NextcloudKit
import OSLog
class ShareController: ObservableObject {
@Published private(set) var share: NKShare
private let kit: NextcloudKit
static func create(
kit: NextcloudKit,
shareType: NKShare.ShareType,
itemServerRelativePath: String,
shareWith: String?,
password: String? = nil,
expireDate: String? = nil,
permissions: Int = 1,
publicUpload: Bool = false,
note: String? = nil,
label: String? = nil,
hideDownload: Bool,
attributes: String? = nil,
options: NKRequestOptions = NKRequestOptions()
) async -> NKError? {
Logger.shareController.info("Creating share: \(itemServerRelativePath)")
return await withCheckedContinuation { continuation in
if shareType == .publicLink {
kit.createShareLink(
path: itemServerRelativePath,
hideDownload: hideDownload,
publicUpload: publicUpload,
password: password,
permissions: permissions,
options: options
) { account, share, data, error in
defer { continuation.resume(returning: error) }
guard error == .success else {
Logger.shareController.error(
"""
Error creating link share: \(error.errorDescription, privacy: .public)
"""
)
return
}
}
} else {
guard let shareWith = shareWith else {
let errorString = "No recipient for share!"
Logger.shareController.error("\(errorString, privacy: .public)")
let error = NKError(statusCode: 0, fallbackDescription: errorString)
continuation.resume(returning: error)
return
}
kit.createShare(
path: itemServerRelativePath,
shareType: shareType.rawValue,
shareWith: shareWith,
password: password,
permissions: permissions,
options: options,
attributes: attributes
) { account, share, data, error in
defer { continuation.resume(returning: error) }
guard error == .success else {
Logger.shareController.error(
"""
Error creating share: \(error.errorDescription, privacy: .public)
"""
)
return
}
}
}
}
}
init(share: NKShare, kit: NextcloudKit) {
self.share = share
self.kit = kit
}
func save(
password: String? = nil,
expireDate: String? = nil,
permissions: Int = 1,
publicUpload: Bool = false,
note: String? = nil,
label: String? = nil,
hideDownload: Bool,
attributes: String? = nil,
options: NKRequestOptions = NKRequestOptions()
) async -> NKError? {
Logger.shareController.info("Saving share: \(self.share.url, privacy: .public)")
return await withCheckedContinuation { continuation in
kit.updateShare(
idShare: share.idShare,
password: password,
expireDate: expireDate,
permissions: permissions,
publicUpload: publicUpload,
note: note,
label: label,
hideDownload: hideDownload,
attributes: attributes,
options: options
) { account, share, data, error in
Logger.shareController.info(
"""
Received update response: \(share?.url ?? "", privacy: .public)
"""
)
defer { continuation.resume(returning: error) }
guard error == .success, let share = share else {
Logger.shareController.error(
"""
Error updating save: \(error.errorDescription, privacy: .public)
"""
)
return
}
self.share = share
}
}
}
func delete() async -> NKError? {
Logger.shareController.info("Deleting share: \(self.share.url, privacy: .public)")
return await withCheckedContinuation { continuation in
kit.deleteShare(idShare: share.idShare) { account, error in
Logger.shareController.info(
"""
Received delete response: \(self.share.url, privacy: .public)
"""
)
defer { continuation.resume(returning: error) }
guard error == .success else {
Logger.shareController.error(
"""
Error deleting save: \(error.errorDescription, privacy: .public)
"""
)
return
}
}
}
}
}

View File

@ -0,0 +1,354 @@
//
// ShareOptionsView.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 28/2/24.
//
import AppKit
import Combine
import NextcloudKit
import OSLog
import SuggestionsTextFieldKit
class ShareOptionsView: NSView {
@IBOutlet private weak var optionsTitleTextField: NSTextField!
@IBOutlet private weak var shareRecipientTextField: NSTextField! // Hide if public link share
@IBOutlet private weak var labelTextField: NSTextField!
@IBOutlet private weak var uploadEditPermissionCheckbox: NSButton!
@IBOutlet private weak var hideDownloadCheckbox: NSButton!
@IBOutlet private weak var passwordProtectCheckbox: NSButton!
@IBOutlet private weak var passwordSecureField: NSSecureTextField!
@IBOutlet private weak var expirationDateCheckbox: NSButton!
@IBOutlet private weak var expirationDatePicker: NSDatePicker!
@IBOutlet private weak var noteForRecipientCheckbox: NSButton!
@IBOutlet private weak var noteTextField: NSTextField!
@IBOutlet private weak var saveButton: NSButton!
@IBOutlet private weak var deleteButton: NSButton!
@IBOutlet private weak var shareTypePicker: NSPopUpButton!
@IBOutlet private weak var publicLinkShareMenuItem: NSMenuItem!
@IBOutlet private weak var userShareMenuItem: NSMenuItem!
@IBOutlet private weak var groupShareMenuItem: NSMenuItem!
@IBOutlet private weak var emailShareMenuItem: NSMenuItem!
@IBOutlet private weak var federatedCloudShareMenuItem: NSMenuItem!
@IBOutlet private weak var circleShare: NSMenuItem!
@IBOutlet private weak var talkConversationShare: NSMenuItem!
var kit: NextcloudKit? {
didSet {
Logger.shareOptionsView.info("Setting up the kit.")
guard let kit = kit else {
Logger.shareOptionsView.error("Could not configure suggestions data source.")
return
}
suggestionsTextFieldDelegate.suggestionsDataSource = ShareeSuggestionsDataSource(
kit: kit
)
suggestionsTextFieldDelegate.confirmationHandler = { suggestion in
guard let sharee = suggestion?.data as? NKSharee else { return }
self.shareRecipientTextField.stringValue = sharee.shareWith
Logger.shareOptionsView.debug("Chose sharee \(sharee.shareWith, privacy: .public)")
}
suggestionsTextFieldDelegate.targetTextField = shareRecipientTextField
}
}
var dataSource: ShareTableViewDataSource?
var controller: ShareController? {
didSet {
guard controller != nil else { return }
optionsTitleTextField.stringValue = "Share options"
deleteButton.title = "Delete"
deleteButton.image = NSImage(
systemSymbolName: "trash", accessibilityDescription: "Delete trash icon"
)
deleteButton.bezelColor = NSColor.systemRed
cancellable?.cancel()
createMode = false
update()
cancellable = controller.publisher.sink { _ in self.update() }
}
}
var createMode = false {
didSet {
Logger.shareOptionsView.info("Create mode set: \(self.createMode, privacy: .public)")
shareTypePicker.isHidden = !createMode
shareRecipientTextField.isHidden = !createMode
labelTextField.isHidden = createMode // Cannot set label on create API call
guard createMode else { return }
optionsTitleTextField.stringValue = "Create new share"
deleteButton.title = "Cancel"
deleteButton.image = NSImage(
systemSymbolName: "xmark.bin", accessibilityDescription: "Cancel create icon"
)
deleteButton.bezelColor = NSColor.controlColor
cancellable?.cancel()
cancellable = nil
controller = nil
reset()
setupCreateForm()
}
}
private var cancellable: AnyCancellable?
private var suggestionsWindowController = SuggestionsWindowController()
private var suggestionsTextFieldDelegate = SuggestionsTextFieldDelegate()
private func update() {
guard let share = controller?.share else {
reset()
setAllFields(enabled: false)
saveButton.isEnabled = false
deleteButton.isEnabled = false
return
}
deleteButton.isEnabled = share.canDelete
saveButton.isEnabled = share.canEdit
setAllFields(enabled: share.canEdit)
reset()
shareRecipientTextField.stringValue = share.shareWithDisplayname
labelTextField.stringValue = share.label
uploadEditPermissionCheckbox.state = share.shareesCanEdit ? .on : .off
hideDownloadCheckbox.state = share.hideDownload ? .on : .off
passwordProtectCheckbox.state = share.password.isEmpty ? .off : .on
passwordSecureField.isHidden = passwordProtectCheckbox.state == .off
passwordSecureField.stringValue = share.password
expirationDateCheckbox.state = share.expirationDate == nil ? .off : .on
expirationDatePicker.isHidden = expirationDateCheckbox.state == .off
expirationDatePicker.dateValue = share.expirationDate as? Date ?? Date()
noteForRecipientCheckbox.state = share.note.isEmpty ? .off : .on
noteTextField.isHidden = noteForRecipientCheckbox.state == .off
noteForRecipientCheckbox.stringValue = share.note
}
private func reset() {
shareRecipientTextField.stringValue = ""
labelTextField.stringValue = ""
uploadEditPermissionCheckbox.state = .off
hideDownloadCheckbox.state = .off
passwordProtectCheckbox.state = .off
passwordSecureField.isHidden = true
passwordSecureField.stringValue = ""
expirationDateCheckbox.state = .off
expirationDatePicker.isHidden = true
expirationDatePicker.dateValue = NSDate.now
expirationDatePicker.minDate = NSDate.now
expirationDatePicker.maxDate = nil
noteForRecipientCheckbox.state = .off
noteTextField.isHidden = true
noteTextField.stringValue = ""
}
private func setupCreateForm() {
guard createMode else { return }
setAllFields(enabled: true)
let type = pickedShareType()
shareRecipientTextField.isHidden = type == .publicLink
if let caps = dataSource?.capabilities?.filesSharing {
uploadEditPermissionCheckbox.state =
caps.defaultPermissions & NKShare.PermissionValues.updateShare.rawValue != 0
? .on : .off
switch type {
case .publicLink:
passwordProtectCheckbox.isHidden = false
passwordProtectCheckbox.state = caps.publicLink?.passwordEnforced == true ? .on : .off
passwordProtectCheckbox.isEnabled = caps.publicLink?.passwordEnforced == false
expirationDateCheckbox.state = caps.publicLink?.expireDateEnforced == true ? .on : .off
expirationDateCheckbox.isEnabled = caps.publicLink?.expireDateEnforced == false
expirationDatePicker.dateValue = Date(
timeIntervalSinceNow:
TimeInterval((caps.publicLink?.expireDateDays ?? 1) * 24 * 60 * 60)
)
if caps.publicLink?.expireDateEnforced == true {
expirationDatePicker.maxDate = expirationDatePicker.dateValue
}
case .email:
passwordProtectCheckbox.isHidden = caps.email?.passwordEnabled == false
passwordProtectCheckbox.state = caps.email?.passwordEnforced == true ? .on : .off
default:
passwordProtectCheckbox.isHidden = true
passwordProtectCheckbox.state = .off
break
}
}
passwordSecureField.isHidden = passwordProtectCheckbox.state == .off
expirationDatePicker.isHidden = expirationDateCheckbox.state == .off
}
private func setAllFields(enabled: Bool) {
shareTypePicker.isEnabled = enabled
shareRecipientTextField.isEnabled = enabled
labelTextField.isEnabled = enabled
uploadEditPermissionCheckbox.isEnabled = enabled
hideDownloadCheckbox.isEnabled = enabled
passwordProtectCheckbox.isEnabled = enabled
passwordSecureField.isEnabled = enabled
expirationDateCheckbox.isEnabled = enabled
expirationDatePicker.isEnabled = enabled
noteForRecipientCheckbox.isEnabled = enabled
noteTextField.isEnabled = enabled
saveButton.isEnabled = enabled
deleteButton.isEnabled = enabled
}
private func pickedShareType() -> NKShare.ShareType {
let selectedShareTypeItem = shareTypePicker.selectedItem
var selectedShareType = NKShare.ShareType.publicLink
if selectedShareTypeItem == publicLinkShareMenuItem {
selectedShareType = .publicLink
} else if selectedShareTypeItem == userShareMenuItem {
selectedShareType = .user
} else if selectedShareTypeItem == groupShareMenuItem {
selectedShareType = .group
} else if selectedShareTypeItem == emailShareMenuItem {
selectedShareType = .email
} else if selectedShareTypeItem == federatedCloudShareMenuItem {
selectedShareType = .federatedCloud
} else if selectedShareTypeItem == circleShare {
selectedShareType = .circle
} else if selectedShareTypeItem == talkConversationShare {
selectedShareType = .talkConversation
}
return selectedShareType
}
@IBAction func shareTypePickerAction(_ sender: Any) {
if createMode {
setupCreateForm()
}
}
@IBAction func passwordCheckboxAction(_ sender: Any) {
passwordSecureField.isHidden = passwordProtectCheckbox.state == .off
}
@IBAction func expirationDateCheckboxAction(_ sender: Any) {
expirationDatePicker.isHidden = expirationDateCheckbox.state == .off
}
@IBAction func noteForRecipientCheckboxAction(_ sender: Any) {
noteTextField.isHidden = noteForRecipientCheckbox.state == .off
}
@IBAction func save(_ sender: Any) {
Task { @MainActor in
let password = passwordProtectCheckbox.state == .on
? passwordSecureField.stringValue
: ""
let expireDate = expirationDateCheckbox.state == .on
? NKShare.formattedDateString(date: expirationDatePicker.dateValue)
: ""
let note = noteForRecipientCheckbox.state == .on
? noteTextField.stringValue
: ""
let label = labelTextField.stringValue
let hideDownload = hideDownloadCheckbox.state == .on
let uploadAndEdit = uploadEditPermissionCheckbox.state == .on
guard !createMode else {
Logger.shareOptionsView.info("Creating new share!")
guard let dataSource = dataSource,
let kit = kit,
let itemServerRelativePath = dataSource.itemServerRelativePath
else {
Logger.shareOptionsView.error("Cannot create new share due to missing data.")
Logger.shareOptionsView.error("dataSource: \(self.dataSource, privacy: .public)")
Logger.shareOptionsView.error("kit: \(self.kit, privacy: .public)")
Logger.shareOptionsView.error(
"path: \(self.dataSource?.itemServerRelativePath ?? "", privacy: .public)"
)
return
}
let selectedShareType = pickedShareType()
let shareWith = shareRecipientTextField.stringValue
var permissions = NKShare.PermissionValues.all.rawValue
permissions = uploadAndEdit
? permissions | NKShare.PermissionValues.updateShare.rawValue
: permissions & ~NKShare.PermissionValues.updateShare.rawValue
setAllFields(enabled: false)
deleteButton.isEnabled = false
saveButton.isEnabled = false
let error = await ShareController.create(
kit: kit,
shareType: selectedShareType,
itemServerRelativePath: itemServerRelativePath,
shareWith: shareWith,
password: password,
expireDate: expireDate,
permissions: permissions,
note: note,
label: label,
hideDownload: hideDownload
)
if let error = error, error != .success {
dataSource.uiDelegate?.showError("Error creating: \(error.errorDescription)")
setAllFields(enabled: true)
} else {
dataSource.uiDelegate?.hideOptions(self)
await dataSource.reload()
}
return
}
Logger.shareOptionsView.info("Editing existing share!")
guard let controller = controller else {
Logger.shareOptionsView.error("No valid share controller, cannot edit share.")
return
}
let share = controller.share
let permissions = uploadAndEdit
? share.permissions | NKShare.PermissionValues.updateShare.rawValue
: share.permissions & ~NKShare.PermissionValues.updateShare.rawValue
setAllFields(enabled: false)
deleteButton.isEnabled = false
saveButton.isEnabled = false
let error = await controller.save(
password: password,
expireDate: expireDate,
permissions: permissions,
note: note,
label: label,
hideDownload: hideDownload
)
if let error = error, error != .success {
dataSource?.uiDelegate?.showError("Error updating share: \(error.errorDescription)")
setAllFields(enabled: true)
} else {
dataSource?.uiDelegate?.hideOptions(self)
await dataSource?.reload()
}
}
}
@IBAction func delete(_ sender: Any) {
Task { @MainActor in
guard !createMode else {
dataSource?.uiDelegate?.hideOptions(self)
reset()
return
}
setAllFields(enabled: false)
deleteButton.isEnabled = false
saveButton.isEnabled = false
let error = await controller?.delete()
if let error = error, error != .success {
dataSource?.uiDelegate?.showError("Error deleting share: \(error.errorDescription)")
}
await dataSource?.reload()
}
}
}

View File

@ -0,0 +1,64 @@
//
// ShareTableItemView.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 28/2/24.
//
import AppKit
import NextcloudKit
class ShareTableItemView: NSTableCellView {
@IBOutlet private weak var typeImageView: NSImageView!
@IBOutlet private weak var label: NSTextField!
@IBOutlet private weak var copyLinkButton: NSButton!
private var originalCopyImage: NSImage?
private var copiedButtonImage: NSImage?
private var tempButtonTimer: Timer?
var share: NKShare? {
didSet {
guard let share = share else {
prepareForReuse()
return
}
typeImageView.image = share.typeImage
label.stringValue = share.displayString
copyLinkButton.isHidden = share.shareType != NKShare.ShareType.publicLink.rawValue
}
}
override func prepareForReuse() {
typeImageView.image = nil
label.stringValue = ""
copyLinkButton.isHidden = false
super.prepareForReuse()
}
@IBAction func copyShareLink(sender: Any) {
guard let share = share else { return }
let pasteboard = NSPasteboard.general
pasteboard.declareTypes([.string], owner: nil)
pasteboard.setString(share.url, forType: .string)
guard tempButtonTimer == nil else { return }
originalCopyImage = copyLinkButton.image
copiedButtonImage = NSImage(
systemSymbolName: "checkmark.circle.fill",
accessibilityDescription: "Public link has been copied icon"
)
var config = NSImage.SymbolConfiguration(scale: .medium)
if #available(macOS 12.0, *) {
config = config.applying(.init(hierarchicalColor: .systemGreen))
}
copiedButtonImage = copiedButtonImage?.withSymbolConfiguration(config)
copyLinkButton.image = copiedButtonImage
tempButtonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { timer in
self.copyLinkButton.image = self.originalCopyImage
self.copiedButtonImage = nil
self.tempButtonTimer?.invalidate()
self.tempButtonTimer = nil
}
}
}

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<tableCellView id="WWf-Il-fKw" customClass="ShareTableItemView" customModule="FileProviderUIExt" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="322" height="42"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wdu-Dj-FUn">
<rect key="frame" x="5" y="5" width="312" height="32"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Ex8-CB-iNc">
<rect key="frame" x="0.0" y="-5" width="32" height="43"/>
<constraints>
<constraint firstAttribute="width" secondItem="Ex8-CB-iNc" secondAttribute="height" multiplier="1:1" id="5ak-dc-HzR"/>
<constraint firstAttribute="height" constant="32" id="Cus-qI-uen"/>
</constraints>
<imageCell key="cell" controlSize="large" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" id="0Xf-Au-QjJ">
<imageReference key="image" image="link.circle.fill" catalog="system" symbolScale="large"/>
</imageCell>
<color key="contentTintColor" name="AccentColor"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MwZ-er-vB4">
<rect key="frame" x="38" y="8" width="236" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" title="Share link" id="Bcz-ws-5yW">
<font key="font" metaFont="systemMedium" size="13"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button horizontalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="y6i-wm-BtQ">
<rect key="frame" x="280" y="0.0" width="32" height="32"/>
<buttonCell key="cell" type="inline" title="Copy share link" bezelStyle="inline" image="doc.on.doc.fill" catalog="system" imagePosition="only" alignment="center" controlSize="large" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ram-fe-8Pt">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystemBold"/>
</buttonCell>
<constraints>
<constraint firstAttribute="width" secondItem="y6i-wm-BtQ" secondAttribute="height" multiplier="1:1" id="BlJ-WU-1y5"/>
</constraints>
<connections>
<action selector="copyShareLinkWithSender:" target="WWf-Il-fKw" id="dgW-8v-wfd"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="Ex8-CB-iNc" firstAttribute="top" secondItem="wdu-Dj-FUn" secondAttribute="top" id="2x8-EU-gHs"/>
<constraint firstItem="y6i-wm-BtQ" firstAttribute="top" secondItem="wdu-Dj-FUn" secondAttribute="top" id="BjV-lg-nyL"/>
<constraint firstAttribute="bottom" secondItem="Ex8-CB-iNc" secondAttribute="bottom" id="dd0-Vh-Pbi"/>
<constraint firstAttribute="bottom" secondItem="y6i-wm-BtQ" secondAttribute="bottom" id="ha0-oR-OxV"/>
<constraint firstItem="MwZ-er-vB4" firstAttribute="centerY" secondItem="Ex8-CB-iNc" secondAttribute="centerY" id="m3A-Tu-qMJ"/>
<constraint firstItem="Ex8-CB-iNc" firstAttribute="leading" secondItem="wdu-Dj-FUn" secondAttribute="leading" id="o1t-TB-uhe"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="wdu-Dj-FUn" firstAttribute="top" secondItem="WWf-Il-fKw" secondAttribute="top" constant="5" id="HIY-8X-dM2"/>
<constraint firstItem="wdu-Dj-FUn" firstAttribute="leading" secondItem="WWf-Il-fKw" secondAttribute="leading" constant="5" id="g7a-hl-t5g"/>
<constraint firstAttribute="bottom" secondItem="wdu-Dj-FUn" secondAttribute="bottom" constant="5" id="gAZ-x8-C4K"/>
<constraint firstAttribute="trailing" secondItem="wdu-Dj-FUn" secondAttribute="trailing" constant="5" id="grc-5X-tMi"/>
</constraints>
<connections>
<outlet property="copyLinkButton" destination="y6i-wm-BtQ" id="8bc-yO-Txo"/>
<outlet property="label" destination="MwZ-er-vB4" id="Gba-jf-C8H"/>
<outlet property="typeImageView" destination="Ex8-CB-iNc" id="T7o-zd-qOo"/>
</connections>
<point key="canvasLocation" x="128" y="-22"/>
</tableCellView>
</objects>
<resources>
<image name="doc.on.doc.fill" catalog="system" width="17" height="19"/>
<image name="link.circle.fill" catalog="system" width="20" height="20"/>
<namedColor name="AccentColor">
<color red="0.0" green="0.46000000000000002" blue="0.89000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,256 @@
//
// ShareTableViewDataSource.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 27/2/24.
//
import AppKit
import FileProvider
import NextcloudKit
import NextcloudFileProviderKit
import NextcloudCapabilitiesKit
import OSLog
class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
private let reattemptInterval: TimeInterval = 3.0
var uiDelegate: ShareViewDataSourceUIDelegate?
var sharesTableView: NSTableView? {
didSet {
sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
sharesTableView?.rowHeight = 42.0 // Height of view in ShareTableItemView XIB
sharesTableView?.dataSource = self
sharesTableView?.delegate = self
sharesTableView?.reloadData()
}
}
var capabilities: Capabilities?
var itemMetadata: NKFile?
private(set) var kit: NextcloudKit?
private(set) var itemURL: URL?
private(set) var itemServerRelativePath: String?
private(set) var shares: [NKShare] = [] {
didSet { Task { @MainActor in sharesTableView?.reloadData() } }
}
private var account: Account? {
didSet {
guard let account = account else { return }
kit = NextcloudKit()
kit?.setup(
user: account.username,
userId: account.username,
password: account.password,
urlBase: account.serverUrl
)
}
}
func loadItem(url: URL) {
itemServerRelativePath = nil
itemURL = url
Task {
await reload()
}
}
func reattempt() {
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in
Task { await self.reload() }
}
}
}
func reload() async {
guard let itemURL = itemURL else { return }
guard let itemIdentifier = await withCheckedContinuation({
(continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
NSFileProviderManager.getIdentifierForUserVisibleFile(
at: itemURL
) { identifier, domainIdentifier, error in
defer { continuation.resume(returning: identifier) }
guard error == nil else {
self.presentError("No item with identifier: \(error.debugDescription)")
return
}
}
}) else {
presentError("Could not get identifier for item, no shares can be acquired.")
return
}
do {
let connection = try await serviceConnection(url: itemURL)
guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
let credentials = await connection.credentials() as? Dictionary<String, String>,
let convertedAccount = Account(dictionary: credentials),
!convertedAccount.password.isEmpty
else {
presentError("Failed to get details from File Provider Extension. Retrying.")
reattempt()
return
}
let serverPathString = serverPath as String
itemServerRelativePath = serverPathString
account = convertedAccount
await sharesTableView?.deselectAll(self)
capabilities = await fetchCapabilities()
guard capabilities != nil else { return }
guard capabilities?.filesSharing?.apiEnabled == true else {
presentError("Server does not support shares.")
return
}
itemMetadata = await fetchItemMetadata(itemRelativePath: serverPathString)
guard itemMetadata?.permissions.contains("R") == true else {
presentError("This file cannot be shared.")
return
}
shares = await fetch(
itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
)
} catch let error {
presentError("Could not reload data: \(error), will try again.")
reattempt()
}
}
private func serviceConnection(url: URL) async throws -> FPUIExtensionService {
let services = try await FileManager().fileProviderServicesForItem(at: url)
guard let service = services[fpUiExtensionServiceName] else {
Logger.sharesDataSource.error("Couldn't get service, required service not present")
throw NSFileProviderError(.providerNotFound)
}
let connection: NSXPCConnection
connection = try await service.fileProviderConnection()
connection.remoteObjectInterface = NSXPCInterface(with: FPUIExtensionService.self)
connection.interruptionHandler = {
Logger.sharesDataSource.error("Service connection interrupted")
}
connection.resume()
guard let proxy = connection.remoteObjectProxy as? FPUIExtensionService else {
throw NSFileProviderError(.serverUnreachable)
}
return proxy
}
private func fetch(
itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
) async -> [NKShare] {
Task { @MainActor in uiDelegate?.fetchStarted() }
defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
let rawIdentifier = itemIdentifier.rawValue
Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
guard let kit = kit else {
self.presentError("NextcloudKit instance is unavailable, cannot fetch shares!")
return []
}
let parameter = NKShareParameter(path: itemRelativePath)
return await withCheckedContinuation { continuation in
kit.readShares(parameters: parameter) { account, shares, data, error in
let shareCount = shares?.count ?? 0
Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
defer { continuation.resume(returning: shares ?? []) }
guard error == .success else {
self.presentError("Error fetching shares: \(error.errorDescription)")
return
}
}
}
}
private func fetchCapabilities() async -> Capabilities? {
return await withCheckedContinuation { continuation in
kit?.getCapabilities { account, capabilitiesJson, error in
guard error == .success, let capabilitiesJson = capabilitiesJson else {
self.presentError("Error getting server caps: \(error.errorDescription)")
continuation.resume(returning: nil)
return
}
Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
continuation.resume(returning: Capabilities(data: capabilitiesJson))
}
}
}
private func fetchItemMetadata(itemRelativePath: String) async -> NKFile? {
guard let kit = kit else {
presentError("Could not fetch item metadata as NextcloudKit instance is unavailable")
return nil
}
func slashlessPath(_ string: String) -> String {
var strCopy = string
if strCopy.hasPrefix("/") {
strCopy.removeFirst()
}
if strCopy.hasSuffix("/") {
strCopy.removeLast()
}
return strCopy
}
let nkCommon = kit.nkCommonInstance
let urlBase = slashlessPath(nkCommon.urlBase)
let davSuffix = slashlessPath(nkCommon.dav)
let userId = nkCommon.userId
let itemRelPath = slashlessPath(itemRelativePath)
let itemFullServerPath = "\(urlBase)/\(davSuffix)/files/\(userId)/\(itemRelPath)"
return await withCheckedContinuation { continuation in
kit.readFileOrFolder(serverUrlFileName: itemFullServerPath, depth: "0") {
account, files, data, error in
guard error == .success else {
self.presentError("Error getting item metadata: \(error.errorDescription)")
continuation.resume(returning: nil)
return
}
Logger.sharesDataSource.info("Successfully retrieved item metadata")
continuation.resume(returning: files.first)
}
}
}
private func presentError(_ errorString: String) {
Logger.sharesDataSource.error("\(errorString, privacy: .public)")
Task { @MainActor in self.uiDelegate?.showError(errorString) }
}
// MARK: - NSTableViewDataSource protocol methods
@objc func numberOfRows(in tableView: NSTableView) -> Int {
shares.count
}
// MARK: - NSTableViewDelegate protocol methods
@objc func tableView(
_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
) -> NSView? {
let share = shares[row]
guard let view = tableView.makeView(
withIdentifier: shareItemViewIdentifier, owner: self
) as? ShareTableItemView else {
Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
return nil
}
view.share = share
return view
}
@objc func tableViewSelectionDidChange(_ notification: Notification) {
guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
Task { @MainActor in uiDelegate?.hideOptions(self) }
return
}
let share = shares[selectedRow]
Task { @MainActor in uiDelegate?.showOptions(share: share) }
}
}

View File

@ -0,0 +1,192 @@
//
// ShareViewController.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 21/2/24.
//
import AppKit
import FileProvider
import NextcloudKit
import OSLog
import QuickLookThumbnailing
class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate {
let shareDataSource = ShareTableViewDataSource()
let itemIdentifiers: [NSFileProviderItemIdentifier]
@IBOutlet weak var fileNameIcon: NSImageView!
@IBOutlet weak var fileNameLabel: NSTextField!
@IBOutlet weak var descriptionLabel: NSTextField!
@IBOutlet weak var createButton: NSButton!
@IBOutlet weak var closeButton: NSButton!
@IBOutlet weak var tableView: NSTableView!
@IBOutlet weak var optionsView: ShareOptionsView!
@IBOutlet weak var splitView: NSSplitView!
@IBOutlet weak var loadingEffectView: NSVisualEffectView!
@IBOutlet weak var loadingIndicator: NSProgressIndicator!
@IBOutlet weak var errorMessageStackView: NSStackView!
@IBOutlet weak var errorTextLabel: NSTextField!
@IBOutlet weak var noSharesLabel: NSTextField!
public override var nibName: NSNib.Name? {
return NSNib.Name(self.className)
}
var actionViewController: DocumentActionViewController! {
return parent as? DocumentActionViewController
}
init(_ itemIdentifiers: [NSFileProviderItemIdentifier]) {
self.itemIdentifiers = itemIdentifiers
super.init(nibName: nil, bundle: nil)
guard let firstItem = itemIdentifiers.first else {
Logger.shareViewController.error("called without items")
closeAction(self)
return
}
Logger.shareViewController.info(
"""
Instantiated with itemIdentifiers:
\(itemIdentifiers.map { $0.rawValue }, privacy: .public)
"""
)
Task {
await processItemIdentifier(firstItem)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
dismissError(self)
hideOptions(self)
}
@IBAction func closeAction(_ sender: Any) {
actionViewController.extensionContext.completeRequest()
}
private func processItemIdentifier(_ itemIdentifier: NSFileProviderItemIdentifier) async {
guard let manager = NSFileProviderManager(for: actionViewController.domain) else {
fatalError("NSFileProviderManager isn't expected to fail")
}
do {
let itemUrl = try await manager.getUserVisibleURL(for: itemIdentifier)
guard itemUrl.startAccessingSecurityScopedResource() else {
Logger.shareViewController.error("Could not access scoped resource for item url!")
return
}
await updateDisplay(itemUrl: itemUrl)
shareDataSource.uiDelegate = self
shareDataSource.sharesTableView = tableView
shareDataSource.loadItem(url: itemUrl)
optionsView.dataSource = shareDataSource
itemUrl.stopAccessingSecurityScopedResource()
} catch let error {
let errorString = "Error processing item: \(error)"
Logger.shareViewController.error("\(errorString, privacy: .public)")
fileNameLabel.stringValue = "Unknown item"
descriptionLabel.stringValue = errorString
}
}
private func updateDisplay(itemUrl: URL) async {
fileNameLabel.stringValue = itemUrl.lastPathComponent
let request = QLThumbnailGenerator.Request(
fileAt: itemUrl,
size: CGSize(width: 128, height: 128),
scale: 1.0,
representationTypes: .icon
)
let generator = QLThumbnailGenerator.shared
let fileThumbnail = await withCheckedContinuation { continuation in
generator.generateRepresentations(for: request) { thumbnail, type, error in
if thumbnail == nil || error != nil {
Logger.shareViewController.error(
"""
Could not get thumbnail: \(error, privacy: .public)
"""
)
}
continuation.resume(returning: thumbnail)
}
}
fileNameIcon.image = fileThumbnail?.nsImage
let resourceValues = try? itemUrl.resourceValues(
forKeys: [.fileSizeKey, .contentModificationDateKey]
)
var sizeDesc = "Unknown size"
var modDesc = "Unknown modification date"
if let fileSize = resourceValues?.fileSize {
sizeDesc = ByteCountFormatter().string(fromByteCount: Int64(fileSize))
}
if let modificationDate = resourceValues?.contentModificationDate {
let modDateString = DateFormatter.localizedString(
from: modificationDate, dateStyle: .short, timeStyle: .short
)
modDesc = "Last modified: \(modDateString)"
}
descriptionLabel.stringValue = "\(sizeDesc) · \(modDesc)"
}
@IBAction func dismissError(_ sender: Any) {
errorMessageStackView.isHidden = true
}
@IBAction func createShare(_ sender: Any) {
guard let kit = shareDataSource.kit else { return }
optionsView.kit = kit
optionsView.createMode = true
tableView.deselectAll(self)
if !splitView.arrangedSubviews.contains(optionsView) {
splitView.addArrangedSubview(optionsView)
optionsView.isHidden = false
}
}
func fetchStarted() {
loadingEffectView.isHidden = false
loadingIndicator.startAnimation(self)
}
func fetchFinished() {
noSharesLabel.isHidden = !shareDataSource.shares.isEmpty
loadingEffectView.isHidden = true
loadingIndicator.stopAnimation(self)
}
func hideOptions(_ sender: Any) {
if sender as? ShareTableViewDataSource == shareDataSource, optionsView.createMode {
// Do not hide options if the table view has had everything deselected when we set the
// options view to be in create mode
return
}
splitView.removeArrangedSubview(optionsView)
optionsView.isHidden = true
}
func showOptions(share: NKShare) {
guard let kit = shareDataSource.kit else { return }
optionsView.kit = kit
optionsView.controller = ShareController(share: share, kit: kit)
if !splitView.arrangedSubviews.contains(optionsView) {
splitView.addArrangedSubview(optionsView)
optionsView.isHidden = false
}
}
func showError(_ errorString: String) {
errorMessageStackView.isHidden = false
errorTextLabel.stringValue = errorString
}
}

View File

@ -0,0 +1,532 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ShareViewController" customModule="FileProviderUIExt" customModuleProvider="target">
<connections>
<outlet property="closeButton" destination="aDA-n9-Zly" id="niR-Ad-FEa"/>
<outlet property="createButton" destination="BMA-BP-wHc" id="HEq-bN-i9V"/>
<outlet property="descriptionLabel" destination="gX0-nE-MrU" id="RoY-u1-1on"/>
<outlet property="errorMessageStackView" destination="dFs-Gh-2WQ" id="kkQ-Uq-xk7"/>
<outlet property="errorTextLabel" destination="770-HW-oC7" id="gfn-SV-TNM"/>
<outlet property="fileNameIcon" destination="zSV-DV-Ray" id="336-e0-CEo"/>
<outlet property="fileNameLabel" destination="slV-H6-zJ3" id="DPp-sN-Yff"/>
<outlet property="loadingEffectView" destination="1ud-mC-gQV" id="HMT-Sb-Axl"/>
<outlet property="loadingIndicator" destination="acb-Yu-Zpm" id="9Jf-dE-7LE"/>
<outlet property="noSharesLabel" destination="K3D-6U-Cbr" id="zDP-E4-9bg"/>
<outlet property="optionsView" destination="EXb-m8-yzj" id="uAb-lv-EZ4"/>
<outlet property="splitView" destination="91w-SP-6sl" id="20T-gQ-SPY"/>
<outlet property="tableView" destination="vb0-a6-eeH" id="KQo-eg-dba"/>
<outlet property="view" destination="Jw6-da-U8j" id="5Ek-F1-w7C"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<view translatesAutoresizingMaskIntoConstraints="NO" id="Jw6-da-U8j">
<rect key="frame" x="0.0" y="0.0" width="500" height="829"/>
<subviews>
<splitView arrangesAllSubviews="NO" dividerStyle="thin" translatesAutoresizingMaskIntoConstraints="NO" id="91w-SP-6sl">
<rect key="frame" x="10" y="10" width="480" height="809"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wGI-UV-bB3">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<subviews>
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9kj-aB-aJh">
<rect key="frame" x="0.0" y="437" width="480" height="43"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="zSV-DV-Ray">
<rect key="frame" x="0.0" y="-3.5" width="43" height="50"/>
<constraints>
<constraint firstAttribute="width" secondItem="zSV-DV-Ray" secondAttribute="height" multiplier="1:1" id="NSH-gA-7lL"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="doc" catalog="system" id="laO-OA-5sJ"/>
</imageView>
<stackView distribution="fill" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="oSY-fV-uws">
<rect key="frame" x="51" y="0.0" width="375" height="43"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="slV-H6-zJ3">
<rect key="frame" x="-2" y="24" width="78" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="File name" id="Uuo-1j-to8">
<font key="font" metaFont="systemBold" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gX0-nE-MrU">
<rect key="frame" x="-2" y="0.0" width="146" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="File size · Last modified" id="1GC-Gr-x29">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<button translatesAutoresizingMaskIntoConstraints="NO" id="BMA-BP-wHc">
<rect key="frame" x="434" y="-3.5" width="18.5" height="51"/>
<buttonCell key="cell" type="bevel" title="Create share" bezelStyle="regularSquare" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="cdh-AC-lt4">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<imageReference key="image" image="plus" catalog="system" symbolScale="large"/>
</buttonCell>
<connections>
<action selector="createShare:" target="-2" id="NLV-ZM-y1w"/>
</connections>
</button>
<button translatesAutoresizingMaskIntoConstraints="NO" id="aDA-n9-Zly">
<rect key="frame" x="460" y="-5" width="20" height="54"/>
<buttonCell key="cell" type="bevel" title="Close" bezelStyle="rounded" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="zNR-DC-3xZ">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<imageReference key="image" image="xmark.circle.fill" catalog="system" symbolScale="large"/>
</buttonCell>
<connections>
<action selector="closeAction:" target="-2" id="D9k-gc-AcN"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="43" id="C79-af-CZw"/>
<constraint firstItem="oSY-fV-uws" firstAttribute="height" secondItem="9kj-aB-aJh" secondAttribute="height" id="I6w-n6-pgZ"/>
<constraint firstAttribute="bottom" secondItem="BMA-BP-wHc" secondAttribute="bottom" id="RE6-Rg-yv9"/>
<constraint firstItem="zSV-DV-Ray" firstAttribute="height" secondItem="9kj-aB-aJh" secondAttribute="height" id="X4V-Vr-Q7m"/>
<constraint firstItem="BMA-BP-wHc" firstAttribute="top" secondItem="9kj-aB-aJh" secondAttribute="top" id="hrc-la-YmY"/>
<constraint firstItem="aDA-n9-Zly" firstAttribute="height" secondItem="9kj-aB-aJh" secondAttribute="height" id="qCh-8Z-jcN"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fillProportionally" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" verticalCompressionResistancePriority="1000" detachesHiddenViews="YES" id="dFs-Gh-2WQ">
<rect key="frame" x="0.0" y="308" width="480" height="40"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="770-HW-oC7">
<rect key="frame" x="0.0" y="0.0" width="411" height="40"/>
<textFieldCell key="cell" selectable="YES" borderStyle="border" alignment="left" title="A long error message that provides detail about why some operations with the shares failed" drawsBackground="YES" id="Jvl-L9-x8K">
<font key="font" metaFont="systemSemibold" size="13"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<color key="backgroundColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button translatesAutoresizingMaskIntoConstraints="NO" id="qfq-F1-w0b">
<rect key="frame" x="416" y="-4" width="67" height="47"/>
<buttonCell key="cell" type="bevel" title="Dismiss" bezelStyle="regularSquare" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="dLP-yX-dyk">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<color key="bezelColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
<connections>
<action selector="dismissError:" target="-2" id="qLa-KM-cYK"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="770-HW-oC7" firstAttribute="top" secondItem="dFs-Gh-2WQ" secondAttribute="top" id="2Y8-H2-wTQ"/>
<constraint firstItem="qfq-F1-w0b" firstAttribute="centerY" secondItem="dFs-Gh-2WQ" secondAttribute="centerY" id="e6n-1Y-kUh"/>
<constraint firstAttribute="bottom" secondItem="770-HW-oC7" secondAttribute="bottom" id="fHi-My-Qyc"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<scrollView autohidesScrollers="YES" horizontalLineScroll="17" horizontalPageScroll="10" verticalLineScroll="17" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZzY-1Z-3xa">
<rect key="frame" x="0.0" y="0.0" width="480" height="300"/>
<clipView key="contentView" drawsBackground="NO" id="Ixg-th-Nw0">
<rect key="frame" x="1" y="1" width="478" height="298"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="firstColumnOnly" columnReordering="NO" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" typeSelect="NO" rowSizeStyle="automatic" viewBased="YES" id="vb0-a6-eeH">
<rect key="frame" x="0.0" y="0.0" width="478" height="298"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn identifier="AutomaticTableColumnIdentifier.0" width="466" minWidth="40" maxWidth="1000" id="3Eb-aD-Ueu">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="mEe-4r-5cx">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
</tableColumn>
</tableColumns>
</tableView>
</subviews>
<nil key="backgroundColor"/>
</clipView>
<constraints>
<constraint firstAttribute="height" constant="300" id="FAW-Ws-zy7"/>
</constraints>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="hQs-hA-bDq">
<rect key="frame" x="1" y="284" width="478" height="15"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="ryi-jk-XFM">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
<constraints>
<constraint firstItem="ZzY-1Z-3xa" firstAttribute="leading" secondItem="wGI-UV-bB3" secondAttribute="leading" id="528-fv-ZfM"/>
<constraint firstItem="9kj-aB-aJh" firstAttribute="top" secondItem="wGI-UV-bB3" secondAttribute="top" id="EOP-84-2ZH"/>
<constraint firstAttribute="trailing" secondItem="ZzY-1Z-3xa" secondAttribute="trailing" id="Kt7-wJ-gb5"/>
<constraint firstItem="9kj-aB-aJh" firstAttribute="leading" secondItem="wGI-UV-bB3" secondAttribute="leading" id="UyT-D1-Awv"/>
<constraint firstAttribute="trailing" secondItem="9kj-aB-aJh" secondAttribute="trailing" id="kua-OU-nbq"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<view translatesAutoresizingMaskIntoConstraints="NO" id="EXb-m8-yzj" customClass="ShareOptionsView" customModule="FileProviderUIExt" customModuleProvider="target">
<rect key="frame" x="0.0" y="481" width="480" height="328"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="5" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T4D-9c-PyA">
<rect key="frame" x="10" y="10" width="460" height="308"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" verticalCompressionResistancePriority="749" translatesAutoresizingMaskIntoConstraints="NO" id="AWy-Qo-wHH">
<rect key="frame" x="-2" y="292" width="464" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Share options" id="fzf-0v-uHo">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aRB-vw-b4r">
<rect key="frame" x="-3" y="263" width="467" height="25"/>
<popUpButtonCell key="cell" type="push" title="Public link share" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="JhA-rv-1xy" id="S60-Qi-URJ">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="yag-Vc-J7Y">
<items>
<menuItem title="Public link share" state="on" id="JhA-rv-1xy"/>
<menuItem title="User share" id="CpL-qc-lAA"/>
<menuItem title="Group share" id="bnp-aV-ZvE"/>
<menuItem title="Email share" id="5DB-JD-Ij0">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Federated cloud share" id="RZP-ME-baz">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Circle share" id="yDE-lS-rJZ">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Talk conversation share" id="aHo-Mr-vTn">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="shareTypePickerAction:" target="EXb-m8-yzj" id="LN7-TC-RvV"/>
</connections>
</popUpButton>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="f8k-Ae-oQc">
<rect key="frame" x="0.0" y="240" width="460" height="22"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Share recipient" bezelStyle="round" id="Ahi-gU-lmO">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="CXW-ZO-B2f">
<rect key="frame" x="0.0" y="213" width="460" height="22"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Share label" bezelStyle="round" id="ZsJ-zc-mFT">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="luZ-Vf-V24">
<rect key="frame" x="-2" y="191" width="175" height="18"/>
<buttonCell key="cell" type="check" title="Allow upload and editing" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="vOP-1k-c75">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2TN-96-apv">
<rect key="frame" x="-2" y="170" width="117" height="18"/>
<buttonCell key="cell" type="check" title="Hide download" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="6Eu-NS-uZ7">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ylD-hi-7Oq">
<rect key="frame" x="-2" y="149" width="132" height="18"/>
<buttonCell key="cell" type="check" title="Password protect" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="MWC-hf-0pc">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="passwordCheckboxAction:" target="EXb-m8-yzj" id="QMn-RM-jdf"/>
</connections>
</button>
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pkv-9L-nhv">
<rect key="frame" x="0.0" y="123" width="460" height="22"/>
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a new password" usesSingleLineMode="YES" bezelStyle="round" id="hcA-we-oYG">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<allowedInputSourceLocales>
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
</allowedInputSourceLocales>
</secureTextFieldCell>
</secureTextField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="HaL-4Z-csA">
<rect key="frame" x="-2" y="101" width="117" height="18"/>
<buttonCell key="cell" type="check" title="Expiration date" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="r1a-iX-8Xo">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="expirationDateCheckboxAction:" target="EXb-m8-yzj" id="KHG-U0-rsG"/>
</connections>
</button>
<datePicker verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="46m-5F-ME5">
<rect key="frame" x="0.0" y="73" width="463" height="28"/>
<datePickerCell key="cell" borderStyle="bezel" alignment="left" useCurrentDate="YES" id="EHU-Gu-Dfh">
<font key="font" metaFont="system"/>
<date key="date" timeIntervalSinceReferenceDate="734115148.45631897">
<!--2024-04-06 16:52:28 +0000-->
</date>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
</datePickerCell>
</datePicker>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="c10-ub-U31">
<rect key="frame" x="-2" y="51" width="132" height="18"/>
<buttonCell key="cell" type="check" title="Note for recipient" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="oLA-Nu-fzO">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="noteForRecipientCheckboxAction:" target="EXb-m8-yzj" id="j7a-Tc-uiu"/>
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="io6-Kg-fLl">
<rect key="frame" x="0.0" y="25" width="460" height="22"/>
<textFieldCell key="cell" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Note for the recipient" bezelStyle="round" id="z5W-BH-NnM">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<stackView distribution="fillEqually" orientation="horizontal" alignment="bottom" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mWb-eX-nfh">
<rect key="frame" x="0.0" y="0.0" width="460" height="20"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bmf-yY-Y7V">
<rect key="frame" x="-7" y="-7" width="240" height="32"/>
<buttonCell key="cell" type="push" title="Delete" bezelStyle="rounded" image="trash.fill" catalog="system" imagePosition="leading" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Kb4-Qg-9Ag">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<color key="bezelColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
<connections>
<action selector="delete:" target="EXb-m8-yzj" id="PZq-SH-QVa"/>
</connections>
</button>
<button horizontalHuggingPriority="249" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PaV-TL-cbq">
<rect key="frame" x="227" y="-7" width="240" height="32"/>
<buttonCell key="cell" type="push" title="Save" bezelStyle="rounded" image="arrow.up.square.fill" catalog="system" imagePosition="trailing" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="O1I-T0-iRC">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<color key="bezelColor" name="AccentColor"/>
<connections>
<action selector="save:" target="EXb-m8-yzj" id="O4N-dj-SlN"/>
</connections>
</button>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="46m-5F-ME5" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="5LB-gI-S8e"/>
<constraint firstItem="io6-Kg-fLl" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="94h-0I-pw6"/>
<constraint firstAttribute="trailing" secondItem="pkv-9L-nhv" secondAttribute="trailing" id="IFt-uJ-etl"/>
<constraint firstAttribute="trailing" secondItem="CXW-ZO-B2f" secondAttribute="trailing" id="Uvk-cg-D23"/>
<constraint firstAttribute="trailing" secondItem="aRB-vw-b4r" secondAttribute="trailing" id="Zp9-vZ-oxU"/>
<constraint firstItem="pkv-9L-nhv" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="bFe-9X-gFR"/>
<constraint firstItem="AWy-Qo-wHH" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="dyr-SA-Kmy"/>
<constraint firstAttribute="trailing" secondItem="46m-5F-ME5" secondAttribute="trailing" id="gV6-Jx-VH4"/>
<constraint firstAttribute="bottom" secondItem="mWb-eX-nfh" secondAttribute="bottom" id="iLe-Zw-oO2"/>
<constraint firstAttribute="trailing" secondItem="io6-Kg-fLl" secondAttribute="trailing" id="mkY-UE-SAy"/>
<constraint firstItem="AWy-Qo-wHH" firstAttribute="top" secondItem="T4D-9c-PyA" secondAttribute="top" id="ndl-52-MnV"/>
<constraint firstItem="CXW-ZO-B2f" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="oWL-JR-HTk"/>
<constraint firstItem="mWb-eX-nfh" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="wiX-kw-Ohy"/>
<constraint firstItem="aRB-vw-b4r" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="xPY-Ll-QCz"/>
<constraint firstAttribute="trailing" secondItem="mWb-eX-nfh" secondAttribute="trailing" id="yUj-je-uXE"/>
<constraint firstAttribute="trailing" secondItem="AWy-Qo-wHH" secondAttribute="trailing" id="zxC-eX-HrE"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="T4D-9c-PyA" firstAttribute="leading" secondItem="EXb-m8-yzj" secondAttribute="leading" constant="10" id="Dhe-wL-ygv"/>
<constraint firstItem="T4D-9c-PyA" firstAttribute="top" secondItem="EXb-m8-yzj" secondAttribute="top" constant="10" id="JrC-o6-So3"/>
<constraint firstAttribute="trailing" secondItem="T4D-9c-PyA" secondAttribute="trailing" constant="10" id="gs6-zf-D6h"/>
<constraint firstAttribute="bottom" secondItem="T4D-9c-PyA" secondAttribute="bottom" constant="10" id="r5C-pm-lv4"/>
</constraints>
<connections>
<outlet property="circleShare" destination="yDE-lS-rJZ" id="yHV-25-RJd"/>
<outlet property="deleteButton" destination="Bmf-yY-Y7V" id="Wd0-LR-DV5"/>
<outlet property="emailShareMenuItem" destination="5DB-JD-Ij0" id="LD5-P8-V2l"/>
<outlet property="expirationDateCheckbox" destination="HaL-4Z-csA" id="vIf-I8-e3i"/>
<outlet property="expirationDatePicker" destination="46m-5F-ME5" id="eGk-fe-IIf"/>
<outlet property="federatedCloudShareMenuItem" destination="RZP-ME-baz" id="quL-N2-y1z"/>
<outlet property="groupShareMenuItem" destination="bnp-aV-ZvE" id="8iU-IP-YXK"/>
<outlet property="hideDownloadCheckbox" destination="2TN-96-apv" id="OPr-4x-aiK"/>
<outlet property="labelTextField" destination="CXW-ZO-B2f" id="otQ-jh-Psr"/>
<outlet property="noteForRecipientCheckbox" destination="c10-ub-U31" id="aG6-4P-cBv"/>
<outlet property="noteTextField" destination="io6-Kg-fLl" id="JKm-A1-SqR"/>
<outlet property="optionsTitleTextField" destination="AWy-Qo-wHH" id="BjX-oW-0Lp"/>
<outlet property="passwordProtectCheckbox" destination="ylD-hi-7Oq" id="qdw-aF-uh2"/>
<outlet property="passwordSecureField" destination="pkv-9L-nhv" id="992-i5-CPF"/>
<outlet property="publicLinkShareMenuItem" destination="JhA-rv-1xy" id="usv-L6-M7k"/>
<outlet property="saveButton" destination="PaV-TL-cbq" id="OvF-Le-oQj"/>
<outlet property="shareRecipientTextField" destination="f8k-Ae-oQc" id="bfc-Vn-Zu9"/>
<outlet property="shareTypePicker" destination="aRB-vw-b4r" id="Dfx-Dw-zEM"/>
<outlet property="talkConversationShare" destination="aHo-Mr-vTn" id="hxF-qw-VQO"/>
<outlet property="uploadEditPermissionCheckbox" destination="luZ-Vf-V24" id="ojW-WP-98U"/>
<outlet property="userShareMenuItem" destination="CpL-qc-lAA" id="R2q-dc-og5"/>
</connections>
</view>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="EXb-m8-yzj" secondAttribute="trailing" id="9yJ-jE-Iin"/>
<constraint firstAttribute="bottom" secondItem="EXb-m8-yzj" secondAttribute="bottom" id="IDB-la-fxl"/>
<constraint firstItem="EXb-m8-yzj" firstAttribute="leading" secondItem="91w-SP-6sl" secondAttribute="leading" id="imf-Yj-oUg"/>
</constraints>
<holdingPriorities>
<real value="250"/>
<real value="250"/>
</holdingPriorities>
</splitView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="K3D-6U-Cbr">
<rect key="frame" x="8" y="474" width="484" height="31"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="No shares" id="byC-34-Vtu">
<font key="font" textStyle="largeTitle" name=".SFNS-Regular"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<visualEffectView blendingMode="withinWindow" material="HUDWindow" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="1ud-mC-gQV">
<rect key="frame" x="10" y="339" width="480" height="300"/>
<subviews>
<progressIndicator wantsLayer="YES" maxValue="100" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="acb-Yu-Zpm">
<rect key="frame" x="224" y="134" width="32" height="32"/>
</progressIndicator>
</subviews>
<constraints>
<constraint firstItem="acb-Yu-Zpm" firstAttribute="centerY" secondItem="1ud-mC-gQV" secondAttribute="centerY" id="Dhf-yv-2Wr"/>
<constraint firstItem="acb-Yu-Zpm" firstAttribute="centerX" secondItem="1ud-mC-gQV" secondAttribute="centerX" id="Kzb-iw-xCx"/>
</constraints>
</visualEffectView>
</subviews>
<constraints>
<constraint firstItem="1ud-mC-gQV" firstAttribute="bottom" secondItem="ZzY-1Z-3xa" secondAttribute="bottom" id="HTR-Xt-ACu"/>
<constraint firstItem="1ud-mC-gQV" firstAttribute="top" secondItem="ZzY-1Z-3xa" secondAttribute="top" id="Hjv-Wk-Rk1"/>
<constraint firstItem="91w-SP-6sl" firstAttribute="top" secondItem="Jw6-da-U8j" secondAttribute="top" constant="10" id="JFe-jp-e9y"/>
<constraint firstAttribute="width" constant="500" id="KBX-aG-ZDU"/>
<constraint firstItem="1ud-mC-gQV" firstAttribute="trailing" secondItem="ZzY-1Z-3xa" secondAttribute="trailing" id="Kpk-6o-ao7"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="380" id="LND-hR-7Co"/>
<constraint firstAttribute="trailing" secondItem="91w-SP-6sl" secondAttribute="trailing" constant="10" id="XrV-hl-kxC"/>
<constraint firstAttribute="bottom" secondItem="91w-SP-6sl" secondAttribute="bottom" constant="10" id="Z4B-85-NR5"/>
<constraint firstItem="1ud-mC-gQV" firstAttribute="height" secondItem="ZzY-1Z-3xa" secondAttribute="height" id="dfF-9z-iG9"/>
<constraint firstItem="91w-SP-6sl" firstAttribute="leading" secondItem="Jw6-da-U8j" secondAttribute="leading" constant="10" id="elm-Nc-tc0"/>
<constraint firstItem="1ud-mC-gQV" firstAttribute="leading" secondItem="ZzY-1Z-3xa" secondAttribute="leading" id="kSz-tV-EFM"/>
<constraint firstItem="K3D-6U-Cbr" firstAttribute="leading" secondItem="ZzY-1Z-3xa" secondAttribute="leading" id="lut-va-B3T"/>
<constraint firstItem="K3D-6U-Cbr" firstAttribute="trailing" secondItem="ZzY-1Z-3xa" secondAttribute="trailing" id="vRC-IM-ovt"/>
<constraint firstItem="K3D-6U-Cbr" firstAttribute="centerY" secondItem="ZzY-1Z-3xa" secondAttribute="centerY" id="wFD-ph-Bsp"/>
</constraints>
<point key="canvasLocation" x="-270" y="79"/>
</view>
</objects>
<resources>
<image name="arrow.up.square.fill" catalog="system" width="15" height="14"/>
<image name="doc" catalog="system" width="14" height="16"/>
<image name="plus" catalog="system" width="18" height="17"/>
<image name="trash.fill" catalog="system" width="15" height="17"/>
<image name="xmark.circle.fill" catalog="system" width="20" height="20"/>
<namedColor name="AccentColor">
<color red="0.0" green="0.46000000000000002" blue="0.89000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,17 @@
//
// ShareViewDataSourceUIDelegate.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 28/2/24.
//
import Foundation
import NextcloudKit
protocol ShareViewDataSourceUIDelegate {
func fetchStarted()
func fetchFinished()
func hideOptions(_ sender: Any)
func showOptions(share: NKShare)
func showError(_ errorString: String)
}

View File

@ -0,0 +1,60 @@
//
// ShareeSuggestionsDataSource.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 2/4/24.
//
import Foundation
import NextcloudKit
import OSLog
import SuggestionsTextFieldKit
class ShareeSuggestionsDataSource: SuggestionsDataSource {
let kit: NextcloudKit
var suggestions: [Suggestion] = []
var inputString: String = "" {
didSet { Task { await updateSuggestions() } }
}
init(kit: NextcloudKit) {
self.kit = kit
}
private func updateSuggestions() async {
let sharees = await fetchSharees(search: inputString)
Logger.shareeDataSource.info("Fetched \(sharees.count, privacy: .public) sharees.")
suggestions = suggestionsFromSharees(sharees)
NotificationCenter.default.post(name: SuggestionsChangedNotificationName, object: self)
}
private func fetchSharees(search: String) async -> [NKSharee] {
Logger.shareeDataSource.debug("Searching sharees with: \(search, privacy: .public)")
return await withCheckedContinuation { continuation in
kit.searchSharees(
search: inputString,
page: 1,
perPage: 20,
completion: { account, sharees, data, error in
defer { continuation.resume(returning: sharees ?? []) }
guard error == .success else {
Logger.shareeDataSource.error(
"Error fetching sharees: \(error.description, privacy: .public)"
)
return
}
}
)
}
}
private func suggestionsFromSharees(_ sharees: [NKSharee]) -> [Suggestion] {
return sharees.map {
Suggestion(
imageName: "person.fill",
displayText: $0.label.isEmpty ? $0.name : $0.label,
data: $0
)
}
}
}

View File

@ -9,24 +9,23 @@
/* Begin PBXBuildFile section */
5307A6E62965C6FA001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E52965C6FA001E0C6A /* NextcloudKit */; };
5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E72965DAD8001E0C6A /* NextcloudKit */; };
5307A6EB2965DB8D001E0C6A /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6EA2965DB8D001E0C6A /* RealmSwift */; };
5307A6F229675346001E0C6A /* NextcloudFilesDatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */; };
5318AD9129BF42FB00CBB71C /* NextcloudItemMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */; };
5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */; };
5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */; };
5318AD9929BF58D000CBB71C /* NKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */; };
531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 531522812B8E01C6002E31BE /* ShareTableItemView.xib */; };
5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */; };
5352B36629DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */; };
5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */; };
5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */; };
5352E85B29B7BFE6002CE85C /* Progress+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */; };
5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */; };
535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */; };
53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */; };
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */; };
536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */; };
536EFC36295E3C1100F4CB13 /* NextcloudAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */; };
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5374FD432B95EE1400C78D54 /* ShareController.swift */; };
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */; };
537630912B85F4980026BFAB /* ShareViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 537630902B85F4980026BFAB /* ShareViewController.xib */; };
537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630922B85F4B00026BFAB /* ShareViewController.swift */; };
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */; };
537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; };
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; };
538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */; };
538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396C27F4765000FA63D5 /* FileProviderExtension.swift */; };
538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396E27F4765000FA63D5 /* FileProviderItem.swift */; };
538E397127F4765000FA63D5 /* FileProviderEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */; };
538E397627F4765000FA63D5 /* FileProviderExt.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 538E396727F4765000FA63D5 /* FileProviderExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
53903D1E2956164F00D0B308 /* NCDesktopClientSocketKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 53903D0E2956164F00D0B308 /* NCDesktopClientSocketKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
53903D212956164F00D0B308 /* NCDesktopClientSocketKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */; };
@ -39,11 +38,17 @@
53903D352956184400D0B308 /* LocalSocketClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 539158B127BE891500816F56 /* LocalSocketClient.h */; settings = {ATTRIBUTES = (Public, ); }; };
53903D37295618A400D0B308 /* LineProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 53903D36295618A400D0B308 /* LineProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; };
539158AC27BE71A900816F56 /* FinderSyncSocketLineProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 539158AB27BE71A900816F56 /* FinderSyncSocketLineProcessor.m */; };
53D056312970594F00988392 /* LocalFilesUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D056302970594F00988392 /* LocalFilesUtils.swift */; };
53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B979802B84C81F002DA742 /* DocumentActionViewController.swift */; };
53C331B22BCD28C30093D38B /* NextcloudFileProviderKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */; };
53C331B62BCD3AFF0093D38B /* NextcloudFileProviderKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */; };
53D666612B70C9A70042C03D /* FileProviderConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D666602B70C9A70042C03D /* FileProviderConfig.swift */; };
53ED472029C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */; };
53ED472829C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */; };
53ED473029C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */; };
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */; };
53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53FE14532B8E1219006C4193 /* NextcloudKit */; };
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */; };
53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */; };
53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */; };
53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */; };
C2B573BA1B1CD91E00303B36 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; };
C2B573D21B1CD94B00303B36 /* main.m in Resources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; };
C2B573DE1B1CD9CE00303B36 /* FinderSync.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573DD1B1CD9CE00303B36 /* FinderSync.m */; };
@ -142,26 +147,23 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudFilesDatabaseManager.swift; sourceTree = "<group>"; };
5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudItemMetadataTable.swift; sourceTree = "<group>"; };
5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudLocalFileMetadataTable.swift; sourceTree = "<group>"; };
5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderMaterialisedEnumerationObserver.swift; sourceTree = "<group>"; };
5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKError+Extensions.swift"; sourceTree = "<group>"; };
531522812B8E01C6002E31BE /* ShareTableItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareTableItemView.xib; sourceTree = "<group>"; };
5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ClientCommunicationProtocol.h; sourceTree = "<group>"; };
5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCommunicationService.swift; sourceTree = "<group>"; };
5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FileProviderExt-Bridging-Header.h"; sourceTree = "<group>"; };
5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudFilesDatabaseManager+Directories.swift"; sourceTree = "<group>"; };
5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudFilesDatabaseManager+LocalFiles.swift"; sourceTree = "<group>"; };
5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+Thumbnailing.swift"; sourceTree = "<group>"; };
5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+Extensions.swift"; sourceTree = "<group>"; };
535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareeSuggestionsDataSource.swift; sourceTree = "<group>"; };
536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderSocketLineProcessor.swift; sourceTree = "<group>"; };
536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudAccount.swift; sourceTree = "<group>"; };
5374FD432B95EE1400C78D54 /* ShareController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareController.swift; sourceTree = "<group>"; };
5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
537630902B85F4980026BFAB /* ShareViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareViewController.xib; sourceTree = "<group>"; };
537630922B85F4B00026BFAB /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionServiceSource.swift; sourceTree = "<group>"; };
537630962B860D920026BFAB /* FPUIExtensionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionService.swift; sourceTree = "<group>"; };
538E396727F4765000FA63D5 /* FileProviderExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
538E396C27F4765000FA63D5 /* FileProviderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderExtension.swift; sourceTree = "<group>"; };
538E396E27F4765000FA63D5 /* FileProviderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderItem.swift; sourceTree = "<group>"; };
538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderEnumerator.swift; sourceTree = "<group>"; };
538E397227F4765000FA63D5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
538E397327F4765000FA63D5 /* FileProviderExt.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderExt.entitlements; sourceTree = "<group>"; };
53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NCDesktopClientSocketKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -172,11 +174,17 @@
539158AB27BE71A900816F56 /* FinderSyncSocketLineProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FinderSyncSocketLineProcessor.m; sourceTree = "<group>"; };
539158B127BE891500816F56 /* LocalSocketClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalSocketClient.h; sourceTree = "<group>"; };
539158B227BEC98A00816F56 /* LocalSocketClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalSocketClient.m; sourceTree = "<group>"; };
53D056302970594F00988392 /* LocalFilesUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFilesUtils.swift; sourceTree = "<group>"; };
53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderUIExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
53B979802B84C81F002DA742 /* DocumentActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentActionViewController.swift; sourceTree = "<group>"; };
53B979852B84C81F002DA742 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
53D666602B70C9A70042C03D /* FileProviderConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderConfig.swift; sourceTree = "<group>"; };
53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderEnumerator+SyncEngine.swift"; sourceTree = "<group>"; };
53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudItemMetadataTable+NKFile.swift"; sourceTree = "<group>"; };
53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+ClientInterface.swift"; sourceTree = "<group>"; };
53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareTableViewDataSource.swift; sourceTree = "<group>"; };
53FE14572B8E3A7C006C4193 /* FileProviderUIExt.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderUIExt.entitlements; sourceTree = "<group>"; };
53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareTableItemView.swift; sourceTree = "<group>"; };
53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKShare+Extensions.swift"; sourceTree = "<group>"; };
53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewDataSourceUIDelegate.swift; sourceTree = "<group>"; };
53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareOptionsView.swift; sourceTree = "<group>"; };
C2B573B11B1CD91E00303B36 /* desktopclient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktopclient.app; sourceTree = BUILT_PRODUCTS_DIR; };
C2B573B51B1CD91E00303B36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C2B573B91B1CD91E00303B36 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
@ -197,10 +205,10 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5307A6EB2965DB8D001E0C6A /* RealmSwift in Frameworks */,
5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */,
538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */,
53903D302956173F00D0B308 /* NCDesktopClientSocketKit.framework in Frameworks */,
53C331B22BCD28C30093D38B /* NextcloudFileProviderKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -211,6 +219,17 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
53B9797B2B84C81F002DA742 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */,
53C331B62BCD3AFF0093D38B /* NextcloudFileProviderKit in Frameworks */,
53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */,
53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C2B573AE1B1CD91E00303B36 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -231,24 +250,13 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5318AD8F29BF406500CBB71C /* Database */ = {
isa = PBXGroup;
children = (
5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */,
5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */,
5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */,
5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */,
53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */,
5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */,
);
path = Database;
sourceTree = "<group>";
};
5350E4C72B0C368B00F276CB /* Services */ = {
isa = PBXGroup;
children = (
5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */,
5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */,
537630962B860D920026BFAB /* FPUIExtensionService.swift */,
537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */,
);
path = Services;
sourceTree = "<group>";
@ -257,8 +265,15 @@
isa = PBXGroup;
children = (
535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */,
5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */,
5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
5376307B2B85E2E00026BFAB /* Extensions */ = {
isa = PBXGroup;
children = (
5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */,
53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -274,20 +289,13 @@
538E396B27F4765000FA63D5 /* FileProviderExt */ = {
isa = PBXGroup;
children = (
5318AD8F29BF406500CBB71C /* Database */,
5352E85929B7BFB4002CE85C /* Extensions */,
5350E4C72B0C368B00F276CB /* Services */,
53D666602B70C9A70042C03D /* FileProviderConfig.swift */,
538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */,
53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */,
538E396C27F4765000FA63D5 /* FileProviderExtension.swift */,
53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */,
5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */,
538E396E27F4765000FA63D5 /* FileProviderItem.swift */,
5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */,
536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */,
53D056302970594F00988392 /* LocalFilesUtils.swift */,
536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */,
538E397327F4765000FA63D5 /* FileProviderExt.entitlements */,
538E397227F4765000FA63D5 /* Info.plist */,
5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */,
@ -306,12 +314,33 @@
path = NCDesktopClientSocketKit;
sourceTree = "<group>";
};
53B9797F2B84C81F002DA742 /* FileProviderUIExt */ = {
isa = PBXGroup;
children = (
5376307B2B85E2E00026BFAB /* Extensions */,
53B979802B84C81F002DA742 /* DocumentActionViewController.swift */,
5374FD432B95EE1400C78D54 /* ShareController.swift */,
53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */,
53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */,
53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */,
531522812B8E01C6002E31BE /* ShareTableItemView.xib */,
53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */,
537630922B85F4B00026BFAB /* ShareViewController.swift */,
537630902B85F4980026BFAB /* ShareViewController.xib */,
53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */,
53FE14572B8E3A7C006C4193 /* FileProviderUIExt.entitlements */,
53B979852B84C81F002DA742 /* Info.plist */,
);
path = FileProviderUIExt;
sourceTree = "<group>";
};
C2B573941B1CD88000303B36 = {
isa = PBXGroup;
children = (
C2B573B31B1CD91E00303B36 /* desktopclient */,
C2B573D81B1CD9CE00303B36 /* FinderSyncExt */,
538E396B27F4765000FA63D5 /* FileProviderExt */,
53B9797F2B84C81F002DA742 /* FileProviderUIExt */,
53903D0D2956164F00D0B308 /* NCDesktopClientSocketKit */,
538E396827F4765000FA63D5 /* Frameworks */,
C2B573B21B1CD91E00303B36 /* Products */,
@ -325,6 +354,7 @@
C2B573D71B1CD9CE00303B36 /* FinderSyncExt.appex */,
538E396727F4765000FA63D5 /* FileProviderExt.appex */,
53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */,
53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */,
);
name = Products;
sourceTree = "<group>";
@ -406,7 +436,7 @@
name = FileProviderExt;
packageProductDependencies = (
5307A6E72965DAD8001E0C6A /* NextcloudKit */,
5307A6EA2965DB8D001E0C6A /* RealmSwift */,
53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */,
);
productName = FileProviderExt;
productReference = 538E396727F4765000FA63D5 /* FileProviderExt.appex */;
@ -430,6 +460,30 @@
productReference = 53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */;
productType = "com.apple.product-type.framework";
};
53B9797D2B84C81F002DA742 /* FileProviderUIExt */ = {
isa = PBXNativeTarget;
buildConfigurationList = 53B979882B84C820002DA742 /* Build configuration list for PBXNativeTarget "FileProviderUIExt" */;
buildPhases = (
53B9797A2B84C81F002DA742 /* Sources */,
53B9797B2B84C81F002DA742 /* Frameworks */,
53B9797C2B84C81F002DA742 /* Resources */,
);
buildRules = (
);
dependencies = (
53FE14522B8E1213006C4193 /* PBXTargetDependency */,
);
name = FileProviderUIExt;
packageProductDependencies = (
53FE14532B8E1219006C4193 /* NextcloudKit */,
5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */,
53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */,
53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */,
);
productName = FileProviderUIExt;
productReference = 53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */;
productType = "com.apple.product-type.app-extension";
};
C2B573B01B1CD91E00303B36 /* desktopclient */ = {
isa = PBXNativeTarget;
buildConfigurationList = C2B573CC1B1CD91E00303B36 /* Build configuration list for PBXNativeTarget "desktopclient" */;
@ -482,7 +536,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1420;
LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1240;
TargetAttributes = {
538E396627F4765000FA63D5 = {
@ -492,6 +546,9 @@
CreatedOnToolsVersion = 14.2;
ProvisioningStyle = Manual;
};
53B9797D2B84C81F002DA742 = {
CreatedOnToolsVersion = 15.2;
};
C2B573B01B1CD91E00303B36 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = 9B5WD74GWJ;
@ -521,7 +578,9 @@
mainGroup = C2B573941B1CD88000303B36;
packageReferences = (
5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */,
5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */,
5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */,
53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */,
53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */,
);
productRefGroup = C2B573B21B1CD91E00303B36 /* Products */;
projectDirPath = "";
@ -530,6 +589,7 @@
C2B573B01B1CD91E00303B36 /* desktopclient */,
C2B573D61B1CD9CE00303B36 /* FinderSyncExt */,
538E396627F4765000FA63D5 /* FileProviderExt */,
53B9797D2B84C81F002DA742 /* FileProviderUIExt */,
53903D0B2956164F00D0B308 /* NCDesktopClientSocketKit */,
);
};
@ -550,6 +610,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
53B9797C2B84C81F002DA742 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */,
537630912B85F4980026BFAB /* ShareViewController.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C2B573AF1B1CD91E00303B36 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -594,27 +663,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5352E85B29B7BFE6002CE85C /* Progress+Extensions.swift in Sources */,
53D666612B70C9A70042C03D /* FileProviderConfig.swift in Sources */,
536EFC36295E3C1100F4CB13 /* NextcloudAccount.swift in Sources */,
53ED473029C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift in Sources */,
538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */,
536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */,
53ED472029C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift in Sources */,
5318AD9929BF58D000CBB71C /* NKError+Extensions.swift in Sources */,
53ED472829C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift in Sources */,
5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */,
537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */,
535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */,
5307A6F229675346001E0C6A /* NextcloudFilesDatabaseManager.swift in Sources */,
53D056312970594F00988392 /* LocalFilesUtils.swift in Sources */,
538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */,
5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */,
5318AD9129BF42FB00CBB71C /* NextcloudItemMetadataTable.swift in Sources */,
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */,
5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */,
5352B36629DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift in Sources */,
5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */,
5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */,
538E397127F4765000FA63D5 /* FileProviderEnumerator.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -626,6 +683,24 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
53B9797A2B84C81F002DA742 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */,
53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */,
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */,
53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */,
53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */,
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */,
53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */,
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */,
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */,
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */,
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C2B573AD1B1CD91E00303B36 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -666,6 +741,10 @@
target = 53903D0B2956164F00D0B308 /* NCDesktopClientSocketKit */;
targetProxy = 53903D322956173F00D0B308 /* PBXContainerItemProxy */;
};
53FE14522B8E1213006C4193 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 53FE14512B8E1213006C4193 /* NextcloudKit */;
};
C2B573E01B1CD9CE00303B36 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C2B573D61B1CD9CE00303B36 /* FinderSyncExt */;
@ -910,6 +989,128 @@
};
name = Release;
};
53B979862B84C81F002DA742 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = FileProviderUIExt/FileProviderUIExt.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = FileProviderUIExt/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_OUTPUT_FORMAT = "same-as-input";
INFOPLIST_PREPROCESS = NO;
IPHONEOS_DEPLOYMENT_TARGET = 17.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = macosx;
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = macOS;
};
name = Debug;
};
53B979872B84C81F002DA742 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = FileProviderUIExt/FileProviderUIExt.entitlements;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = FileProviderUIExt/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_OUTPUT_FORMAT = "same-as-input";
INFOPLIST_PREPROCESS = NO;
IPHONEOS_DEPLOYMENT_TARGET = 17.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = macosx;
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = macOS;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
C2B573991B1CD88000303B36 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -1216,6 +1417,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
53B979882B84C820002DA742 /* Build configuration list for PBXNativeTarget "FileProviderUIExt" */ = {
isa = XCConfigurationList;
buildConfigurations = (
53B979862B84C81F002DA742 /* Debug */,
53B979872B84C81F002DA742 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C2B573981B1CD88000303B36 /* Build configuration list for PBXProject "NextcloudIntegration" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -1250,16 +1460,32 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nextcloud/NextcloudKit.git";
requirement = {
branch = develop;
kind = branch;
kind = upToNextMajorVersion;
minimumVersion = 2.5.9;
};
};
5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */ = {
5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/realm/realm-swift.git";
repositoryURL = "https://github.com/claucambra/NextcloudCapabilitiesKit.git";
requirement = {
kind = exactVersion;
version = 10.33.0;
kind = upToNextMajorVersion;
minimumVersion = 2.0.0;
};
};
53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/claucambra/SuggestionsTextFieldKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/claucambra/NextcloudFileProviderKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.0;
};
};
/* End XCRemoteSwiftPackageReference section */
@ -1275,10 +1501,35 @@
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
5307A6EA2965DB8D001E0C6A /* RealmSwift */ = {
5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */;
productName = RealmSwift;
package = 5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */;
productName = NextcloudCapabilitiesKit;
};
53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */ = {
isa = XCSwiftPackageProductDependency;
package = 53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */;
productName = SuggestionsTextFieldKit;
};
53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */ = {
isa = XCSwiftPackageProductDependency;
package = 53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */;
productName = NextcloudFileProviderKit;
};
53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */ = {
isa = XCSwiftPackageProductDependency;
package = 53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */;
productName = NextcloudFileProviderKit;
};
53FE14512B8E1213006C4193 /* NextcloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
53FE14532B8E1219006C4193 /* NextcloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
/* End XCSwiftPackageProductDependency section */
};

View File

@ -25,8 +25,13 @@ macro(libcloudproviders_add_config _sources)
endmacro(libcloudproviders_add_config _sources)
find_package(Qt5 5.15 COMPONENTS DBus)
IF (Qt5DBus_FOUND)
if (Qt6_FOUND)
find_package(Qt6 COMPONENTS COMPONENTS DBus)
else()
set(REQUIRED_QT_VERSION "5.15.0")
find_package(Qt5 ${REQUIRED_QT_VERSION} COMPONENTS DBus)
endif()
IF (Qt5DBus_FOUND OR Qt6DBus_FOUND)
STRING(TOLOWER "${APPLICATION_VENDOR}" DBUS_VENDOR)
STRING(REGEX REPLACE "[^A-z0-9]" "" DBUS_VENDOR "${DBUS_VENDOR}")
STRING(REGEX REPLACE "[^A-z0-9]" "" DBUS_APPLICATION_NAME "${APPLICATION_SHORTNAME}")

View File

@ -1,261 +0,0 @@
/****************************************************************************
**
** Copyright (C) 2014 Daniel Molkentin <daniel@molkentin.de>
** Contact: http://www.qt-project.org/legal
**
** This file is part of the QtNetwork module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia. For licensing terms and
** conditions see http://qt.digia.com/licensing. For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef TOKENIZER_H
#define TOKENIZER_H
#include <QString>
#include <QByteArray>
#include <QSharedPointer>
QT_BEGIN_NAMESPACE
template <class T, class const_iterator>
struct QTokenizerPrivate {
using char_type = typename T::value_type;
struct State {
bool inQuote = false;
bool inEscape = false;
char_type quoteChar = '\0';
};
QTokenizerPrivate(const T& _string, const T& _delims) :
string(_string)
, begin(string.begin())
, end(string.end())
, tokenBegin(end)
, tokenEnd(begin)
, delimiters(_delims)
{
}
[[nodiscard]] bool isDelimiter(char_type c) const {
return delimiters.contains(c);
}
[[nodiscard]] bool isQuote(char_type c) const {
return quotes.contains(c);
}
// Returns true if a delimiter was not hit
bool nextChar(State* state, char_type c) {
if (state->inQuote) {
if (state->inEscape) {
state->inEscape = false;
} else if (c == '\\') {
state->inEscape = true;
} else if (c == state->quoteChar) {
state->inQuote = false;
}
} else {
if (isDelimiter(c))
return false;
state->inQuote = isQuote(state->quoteChar = c);
}
return true;
}
T string;
// ### copies begin and end for performance, premature optimization?
const_iterator begin;
const_iterator end;
const_iterator tokenBegin;
const_iterator tokenEnd;
T delimiters;
T quotes;
bool isDelim = false;
bool returnDelimiters = false;
bool returnQuotes = false;
};
template <class T, class const_iterator = typename T::const_iterator>
class QTokenizer {
public:
using char_type = typename T::value_type;
/*!
\class QTokenizer
\inmodule QtNetwork
\brief QTokenizer tokenizes Strings on QString, QByteArray,
std::string or std::wstring
Example Usage:
\code
QString str = ...;
QByteArrayTokenizer tokenizer(str, "; ");
tokenizer.setQuoteCharacters("\"'");
tokenizer.setReturnDelimiters(true);
while (tokenizer.hasNext()) {
QByteArray token = tokenizer.next();
bool isDelimiter = tokenizer.isDelimiter();
...
}
\endcode
\param string The string to tokenize
\param delimiters A string containing delimiters
\sa QStringTokenizer, QByteArrayTokenizer, StringTokenizer, WStringTokenizer
*/
QTokenizer(const T& string, const T& delimiters)
: d(new QTokenizerPrivate<T, const_iterator>(string, delimiters))
{ }
/*!
Whether or not to return delimiters as tokens
\see setQuoteCharacters
*/
void setReturnDelimiters(bool enable) { d->returnDelimiters = enable; }
/*!
Sets characters that are considered to start and end quotes.
When between two characters considered a quote, delimiters will
be ignored.
When between quotes, blackslash characters will cause the QTokenizer
to skip the next character.
\param quotes Characters that delimit quotes.
*/
void setQuoteCharacters(const T& quotes) { d->quotes = quotes; }
/*!
Whether or not to return delimiters as tokens
\see setQuoteCharacters
*/
void setReturnQuoteCharacters(bool enable) { d->returnQuotes = enable; }
/*!
Retrieve next token.
Returns true if there are more tokens, false otherwise.
\sa next()
*/
bool hasNext()
{
typename QTokenizerPrivate<T, const_iterator>::State state;
d->isDelim = false;
for (;;) {
d->tokenBegin = d->tokenEnd;
if (d->tokenEnd == d->end)
return false;
d->tokenEnd++;
if (d->nextChar(&state, *d->tokenBegin))
break;
if (d->returnDelimiters) {
d->isDelim = true;
return true;
}
}
while (d->tokenEnd != d->end && d->nextChar(&state, *d->tokenEnd)) {
d->tokenEnd++;
}
return true;
}
/*!
Resets the tokenizer to the starting position.
*/
void reset() {
d->tokenEnd = d->begin;
}
/*!
Returns true if the current token is a delimiter,
if one more more delimiting characters have been set.
*/
[[nodiscard]] bool isDelimiter() const { return d->isDelim; }
/*!
Returns the current token.
Use \c hasNext() to fetch the next token.
*/
[[nodiscard]] T next() const {
int len = std::distance(d->tokenBegin, d->tokenEnd);
const_iterator tmpStart = d->tokenBegin;
if (!d->returnQuotes && len > 1 && d->isQuote(*d->tokenBegin)) {
tmpStart++;
len -= 2;
}
return T(tmpStart, len);
}
private:
friend class QStringTokenizer;
QSharedPointer<QTokenizerPrivate<T, const_iterator> > d;
};
class QStringTokenizer : public QTokenizer<QString> {
public:
QStringTokenizer(const QString &string, const QString &delim) :
QTokenizer<QString, QString::const_iterator>(string, delim) {}
/**
* @brief Like \see next(), but returns a lightweight string reference
* @return A reference to the token within the string
*/
QStringRef stringRef() {
// If those differences overflow an int we'd have a veeeeeery long string in memory
int begin = std::distance(d->begin, d->tokenBegin);
int end = std::distance(d->tokenBegin, d->tokenEnd);
if (!d->returnQuotes && d->isQuote(*d->tokenBegin)) {
begin++;
end -= 2;
}
return QStringRef(&d->string, begin, end);
}
};
using QByteArrayTokenizer = QTokenizer<QByteArray>;
using StringTokenizer = QTokenizer<std::string>;
using WStringTokenizer = QTokenizer<std::wstring>;
QT_END_NAMESPACE
#endif // TOKENIZER_H

View File

@ -1,2 +0,0 @@
TEMPLATE = subdirs
SUBDIRS = test

View File

@ -1,8 +0,0 @@
TEMPLATE = app
QT += testlib
CONFIG += testlib
TARGET = test
INCLUDEPATH += . ..
# Input
SOURCES += tst_qtokenizer.cpp

View File

@ -1,139 +0,0 @@
#include <QtTest>
#include "qtokenizer.h"
namespace {
const QString simple = QLatin1String("A simple tokenizer test");
const QString quoted = QLatin1String("\"Wait for me!\" he shouted");
}
class TestTokenizer : public QObject
{
Q_OBJECT
private slots:
void tokenizeQStringSimple() {
QStringTokenizer tokenizer(simple, " ");
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("A"));
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("simple"));
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("tokenizer"));
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("test"));
QCOMPARE(tokenizer.hasNext(), false);
}
void tokenizeQStringSimpleRef() {
QStringTokenizer tokenizer(simple, " ");
QCOMPARE(tokenizer.hasNext(), true);
QVERIFY(tokenizer.stringRef() == QLatin1String("A"));
QCOMPARE(tokenizer.hasNext(), true);
QVERIFY(tokenizer.stringRef() == QLatin1String("simple"));
QCOMPARE(tokenizer.hasNext(), true);
QVERIFY(tokenizer.stringRef() == QLatin1String("tokenizer"));
QCOMPARE(tokenizer.hasNext(), true);
QVERIFY(tokenizer.stringRef() == QLatin1String("test"));
QCOMPARE(tokenizer.hasNext(), false);
}
void tokenizeQStringQuoted() {
const QString multiquote(QLatin1String("\"'Billy - the Kid' is dead!\""));
QStringTokenizer tokenizer(multiquote, " -");
tokenizer.setQuoteCharacters("\"");
tokenizer.setReturnQuoteCharacters(true);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("\"'Billy - the Kid' is dead!\""));
QCOMPARE(tokenizer.hasNext(), false);
}
void tokenizeQStringSkipQuotes() {
const QString multiquote(QLatin1String("\"'Billy - the Kid' is dead!\""));
QStringTokenizer tokenizer(multiquote, " ");
tokenizer.setQuoteCharacters("\"");
tokenizer.setReturnQuoteCharacters(false);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("'Billy - the Kid' is dead!"));
QCOMPARE(tokenizer.stringRef().toString(), QLatin1String("'Billy - the Kid' is dead!"));
QCOMPARE(tokenizer.hasNext(), false);
}
void tokenizeQStringWithDelims() {
const QString delims(QLatin1String("I;Insist,On/a-Delimiter"));
QStringTokenizer tokenizer(delims, ";,/-");
tokenizer.setReturnDelimiters(true);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), false);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), true);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), false);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), true);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), false);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), true);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), false);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), true);
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.isDelimiter(), false);
QCOMPARE(tokenizer.hasNext(), false);
}
void resetTokenizer() {
for (int i = 0; i < 2; i++) {
QStringTokenizer tokenizer(simple, " ");
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("A"));
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("simple"));
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("tokenizer"));
QCOMPARE(tokenizer.hasNext(), true);
QCOMPARE(tokenizer.next(), QLatin1String("test"));
QCOMPARE(tokenizer.hasNext(), false);
tokenizer.reset();
}
}
// ### QByteArray, other types
};
QTEST_APPLESS_MAIN(TestTokenizer)
#include "tst_qtokenizer.moc"

View File

@ -2,71 +2,77 @@ include(ECMEnableSanitizers)
set(REQUIRED_QT_VERSION "5.15.0")
find_package(Qt5Core ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt5Core PROPERTIES
DESCRIPTION "Qt5 Core component."
find_package(Qt${QT_MAJOR_VERSION}Core ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt${QT_MAJOR_VERSION}Core PROPERTIES
DESCRIPTION "Qt${QT_MAJOR_VERSION} Core component."
TYPE REQUIRED
)
find_package(Qt5Network ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt5Network PROPERTIES
DESCRIPTION "Qt5 Network component."
find_package(Qt${QT_MAJOR_VERSION}Network ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt${QT_MAJOR_VERSION}Network PROPERTIES
DESCRIPTION "Qt${QT_MAJOR_VERSION} Network component."
TYPE REQUIRED
)
find_package(Qt5Xml ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt5Xml PROPERTIES
DESCRIPTION "Qt5 Xml component."
find_package(Qt${QT_MAJOR_VERSION}Xml ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt${QT_MAJOR_VERSION}Xml PROPERTIES
DESCRIPTION "Qt${QT_MAJOR_VERSION} Xml component."
TYPE REQUIRED
)
find_package(Qt5Concurrent ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt5Concurrent PROPERTIES
DESCRIPTION "Qt5 Concurrent component."
find_package(Qt${QT_MAJOR_VERSION}Concurrent ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt${QT_MAJOR_VERSION}Concurrent PROPERTIES
DESCRIPTION "Qt${QT_MAJOR_VERSION} Concurrent component."
TYPE REQUIRED
)
find_package(Qt5QuickWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt5QuickWidgets PROPERTIES
DESCRIPTION "Qt5 QuickWidgets component."
find_package(Qt${QT_MAJOR_VERSION}QuickWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt${QT_MAJOR_VERSION}QuickWidgets PROPERTIES
DESCRIPTION "Qt${QT_MAJOR_VERSION} QuickWidgets component."
TYPE REQUIRED
)
find_package(Qt5WebEngineWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET)
find_package(Qt${QT_VERSION_MAJOR}Core5Compat ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt${QT_VERSION_MAJOR}Core5Compat PROPERTIES
DESCRIPTION "Qt${QT_VERSION_MAJOR} Core5Compat component."
TYPE REQUIRED
)
find_package(Qt${QT_MAJOR_VERSION}WebEngineWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET)
if(NOT BUILD_WITH_WEBENGINE)
set_package_properties(Qt5WebEngineWidgets PROPERTIES
DESCRIPTION "Qt5 WebEngineWidgets component."
set_package_properties(Qt${QT_MAJOR_VERSION}WebEngineWidgets PROPERTIES
DESCRIPTION "Qt${QT_MAJOR_VERSION} WebEngineWidgets component."
TYPE RECOMMENDED
)
else()
set_package_properties(Qt5WebEngineWidgets PROPERTIES
DESCRIPTION "Qt5 WebEngineWidgets component."
set_package_properties(Qt${QT_MAJOR_VERSION}WebEngineWidgets PROPERTIES
DESCRIPTION "Qt${QT_MAJOR_VERSION} WebEngineWidgets component."
TYPE REQUIRED
)
endif()
find_package(Qt5WebEngine ${REQUIRED_QT_VERSION} CONFIG QUIET)
if(NOT BUILD_WITH_WEBENGINE)
set_package_properties(Qt5WebEngine PROPERTIES
DESCRIPTION "Qt5 WebEngine component."
find_package(Qt${QT_VERSION_MAJOR}WebEngineCore ${REQUIRED_QT_VERSION} CONFIG QUIET)
if(APPLE)
set_package_properties(Qt${QT_VERSION_MAJOR}WebEngineCore PROPERTIES
DESCRIPTION "Qt${QT_VERSION_MAJOR} WebEngineCore component."
TYPE RECOMMENDED
)
else()
set_package_properties(Qt5WebEngine PROPERTIES
DESCRIPTION "Qt5 WebEngine component."
set_package_properties(Qt${QT_VERSION_MAJOR}WebEngine PROPERTIES
DESCRIPTION "Qt${QT_VERSION_MAJOR} WebEngine component."
TYPE REQUIRED
)
endif()
if(BUILD_WITH_WEBENGINE AND Qt5WebEngine_FOUND AND Qt5WebEngineWidgets_FOUND)
if(Qt${QT_MAJOR_VERSION}WebEngine_FOUND AND Qt${QT_MAJOR_VERSION}WebEngineWidgets_FOUND)
add_compile_definitions(WITH_WEBENGINE=1)
endif()
get_target_property (QT_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION)
message(STATUS "Using Qt ${Qt5Core_VERSION} (${QT_QMAKE_EXECUTABLE})")
get_target_property (QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION)
message(STATUS "Using Qt ${Qt${QT_MAJOR_VERSION}Core_VERSION} (${QT_QMAKE_EXECUTABLE})")
if(NOT TOKEN_AUTH_ONLY)
find_package(Qt5Keychain REQUIRED)
find_package(Qt${QT_MAJOR_VERSION}Keychain REQUIRED)
endif()
# TODO: Mingw64 7.3 might also need to be excluded here as it seems to not automatically link libssp

View File

@ -10,13 +10,10 @@ add_library(cmdCore STATIC
target_link_libraries(cmdCore
PUBLIC
Nextcloud::sync
Qt5::Core
Qt5::Network
Qt::Core
Qt::Network
)
# Need tokenizer for netrc parser
target_include_directories(cmdCore PRIVATE ${CMAKE_SOURCE_DIR}/src/3rdparty/qtokenizer)
if(UNIX AND NOT APPLE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIE")

View File

@ -15,8 +15,7 @@
#include <QDir>
#include <QFile>
#include <QTextStream>
#include <qtokenizer.h>
#include <QStringTokenizer>
#include <QDebug>
@ -59,33 +58,32 @@ bool NetrcParser::parse()
}
QString content = netrc.readAll();
QStringTokenizer tokenizer(content, " \n\t");
tokenizer.setQuoteCharacters("\"'");
auto tokenizer = QStringTokenizer{content, u" \n\t"};
LoginPair pair;
QString machine;
bool isDefault = false;
while (tokenizer.hasNext()) {
QString key = tokenizer.next();
for(auto itToken = tokenizer.cbegin(); itToken != tokenizer.cend(); ++itToken) {
const auto key = *itToken;
if (key == defaultKeyword) {
tryAddEntryAndClear(machine, pair, isDefault);
isDefault = true;
continue; // don't read a value
}
if (!tokenizer.hasNext()) {
if (itToken != tokenizer.cend()) {
qDebug() << "error fetching value for" << key;
return false;
}
QString value = tokenizer.next();
auto value = *(++itToken);
if (key == machineKeyword) {
tryAddEntryAndClear(machine, pair, isDefault);
machine = value;
machine = value.toString();
} else if (key == loginKeyword) {
pair.first = value;
pair.first = value.toString();
} else if (key == passwordKeyword) {
pair.second = value;
pair.second = value.toString();
} // ignore unsupported tokens
}
tryAddEntryAndClear(machine, pair, isDefault);

View File

@ -18,6 +18,7 @@
#include "remotepermissions.h"
#include <QVariant>
#include <QLoggingCategory>
#include <cstring>

View File

@ -586,7 +586,7 @@ bool SyncJournalDb::checkConnect()
createQuery.bindValue(1, MIRALL_VERSION_MAJOR);
createQuery.bindValue(2, MIRALL_VERSION_MINOR);
createQuery.bindValue(3, MIRALL_VERSION_PATCH);
createQuery.bindValue(4, MIRALL_VERSION_BUILD);
createQuery.bindValue(4, static_cast<qulonglong>(MIRALL_VERSION_BUILD));
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Update version"), createQuery);
}
@ -616,7 +616,7 @@ bool SyncJournalDb::checkConnect()
createQuery.bindValue(1, MIRALL_VERSION_MAJOR);
createQuery.bindValue(2, MIRALL_VERSION_MINOR);
createQuery.bindValue(3, MIRALL_VERSION_PATCH);
createQuery.bindValue(4, MIRALL_VERSION_BUILD);
createQuery.bindValue(4, static_cast<qulonglong>(MIRALL_VERSION_BUILD));
createQuery.bindValue(5, major);
createQuery.bindValue(6, minor);
createQuery.bindValue(7, patch);

View File

@ -209,7 +209,7 @@ qint64 Utility::freeDiskSpace(const QString &path)
QString Utility::compactFormatDouble(double value, int prec, const QString &unit)
{
QLocale locale = QLocale::system();
QChar decPoint = locale.decimalPoint();
const auto decPoint = locale.decimalPoint();
QString str = locale.toString(value, 'f', prec);
while (str.endsWith(QLatin1Char('0')) || str.endsWith(decPoint)) {
if (str.endsWith(decPoint)) {
@ -575,7 +575,7 @@ QString Utility::makeConflictFileName(
bool Utility::isConflictFile(const QString &name)
{
auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
auto bname = name.mid(name.lastIndexOf(QLatin1Char('/')) + 1);
if (bname.contains(QStringLiteral("_conflict-"))) {
return true;
@ -676,7 +676,7 @@ QString Utility::makeCaseClashConflictFileName(const QString &filename, const QD
bool Utility::isCaseClashConflictFile(const QString &name)
{
const auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
const auto bname = name.mid(name.lastIndexOf(QLatin1Char('/')) + 1);
return bname.contains(QStringLiteral("(case clash from"));
}

View File

@ -22,7 +22,6 @@
#include <QCoreApplication>
#include <QDir>
#include <QLoggingCategory>
#include <QtMacExtras/QtMacExtras>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>

View File

@ -99,7 +99,6 @@ void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName,
const QString executablePath = runningInsideAppImage ? appImagePath : QCoreApplication::applicationFilePath();
QTextStream ts(&iniFile);
ts.setCodec("UTF-8");
ts << QLatin1String("[Desktop Entry]\n")
<< QLatin1String("Name=") << guiName << QLatin1Char('\n')
<< QLatin1String("GenericName=") << QLatin1String("File Synchronizer\n")

View File

@ -187,7 +187,11 @@ public:
* If the remote metadata changes, the local placeholder's metadata should possibly
* change as well.
*/
Q_REQUIRED_RESULT virtual Result<void, QString> updateMetadata(const QString &filePath, time_t modtime, qint64 size, const QByteArray &fileId) = 0;
[[nodiscard]] virtual Result<void, QString> updateMetadata(const QString &filePath, time_t modtime, qint64 size, const QByteArray &fileId) = 0;
[[nodiscard]] virtual Result<Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderMarkInSync(const QString &filePath, const QByteArray &fileId) = 0;
[[nodiscard]] virtual bool isPlaceHolderInSync(const QString &filePath) const = 0;
/// Create a new dehydrated placeholder. Called from PropagateDownload.
Q_REQUIRED_RESULT virtual Result<void, QString> createPlaceholder(const SyncFileItem &item) = 0;
@ -325,6 +329,8 @@ public:
[[nodiscard]] bool isHydrating() const override { return false; }
Result<void, QString> updateMetadata(const QString &, time_t, qint64, const QByteArray &) override { return {}; }
Result<Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderMarkInSync(const QString &filePath, const QByteArray &fileId) override {Q_UNUSED(filePath) Q_UNUSED(fileId) return {QString{}};}
[[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; }
Result<void, QString> createPlaceholder(const SyncFileItem &) override { return {}; }
Result<void, QString> dehydratePlaceholder(const SyncFileItem &) override { return {}; }
Result<ConvertToPlaceholderResult, QString> convertToPlaceholder(const QString &, const SyncFileItem &, const QString &, const UpdateMetadataTypes) override { return ConvertToPlaceholderResult::Ok; }

View File

@ -24,14 +24,14 @@ if(NOT BUILD_LIBRARIES_ONLY)
${crashreporter_RC_RCC}
)
find_package(Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_MAJOR_VERSION} REQUIRED COMPONENTS Widgets)
target_include_directories(${CRASHREPORTER_EXECUTABLE} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
set_target_properties(${CRASHREPORTER_EXECUTABLE} PROPERTIES AUTOMOC ON)
set_target_properties(${CRASHREPORTER_EXECUTABLE} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_OUTPUT_DIRECTORY} )
target_link_libraries(${CRASHREPORTER_EXECUTABLE}
crashreporter-gui
Qt5::Core Qt5::Widgets
Qt::Core Qt::Widgets
)
if(BUILD_OWNCLOUD_OSX_BUNDLE)
@ -48,7 +48,7 @@ if(NOT BUILD_LIBRARIES_ONLY)
# currently it needs to be done because the code right above needs to be executed no matter
# if building a bundle or not and the install_qt4_executable needs to be called afterwards
if(BUILD_OWNCLOUD_OSX_BUNDLE)
get_target_property (QT_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION)
get_target_property (QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION)
get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY)
find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${QT_BIN_DIR}")

View File

@ -72,7 +72,7 @@ generate_export_header(nextcloud_csync
target_link_libraries(nextcloud_csync
PUBLIC
${CSYNC_REQUIRED_LIBRARIES}
Qt5::Core Qt5::Concurrent
Qt::Core Qt::Concurrent
)
if(ZLIB_FOUND)

View File

@ -43,6 +43,8 @@
#include <functional>
#include <memory>
#include <QByteArray>
#include <QVariant>
#include "common/remotepermissions.h"
namespace OCC {
@ -153,6 +155,7 @@ enum SyncInstructions {
CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
but without any propagation (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */
CSYNC_INSTRUCTION_UPDATE_VFS_METADATA = 1 << 13, /* vfs item metadata are out of sync and we need to tell operating system about it */
};
Q_ENUM_NS(SyncInstructions)

View File

@ -36,6 +36,7 @@
#include <QString>
#include <QFileInfo>
#include <QDir>
#include <QVariant>
/** Expands C-like escape sequences (in place)
*/
@ -90,7 +91,7 @@ static const char *win_reserved_words_n[] = { "CLOCK$", "$Recycle.Bin" };
* @param file_name filename
* @return true if file is reserved, false otherwise
*/
OCSYNC_EXPORT bool csync_is_windows_reserved_word(const QStringRef &filename)
OCSYNC_EXPORT bool csync_is_windows_reserved_word(const QStringView &filename)
{
size_t len_filename = filename.size();
@ -132,10 +133,10 @@ OCSYNC_EXPORT bool csync_is_windows_reserved_word(const QStringRef &filename)
static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const QString &path, bool excludeConflictFiles)
{
/* split up the path */
QStringRef bname(&path);
int lastSlash = path.lastIndexOf(QLatin1Char('/'));
QStringView bname(path);
int lastSlash = bname.lastIndexOf(QLatin1Char('/'));
if (lastSlash >= 0) {
bname = path.midRef(lastSlash + 1);
bname = bname.mid(lastSlash + 1);
}
qsizetype blen = bname.size();
@ -450,10 +451,10 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const QString &path, Ite
// Check the bname part of the path to see whether the full
// regex should be run.
QStringRef bnameStr(&path);
QStringView bnameStr(path);
int lastSlash = path.lastIndexOf(QLatin1Char('/'));
if (lastSlash >= 0) {
bnameStr = path.midRef(lastSlash + 1);
bnameStr = bnameStr.mid(lastSlash + 1);
}
QString basePath(_localPath + path);

View File

@ -15,7 +15,7 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtGraphicalEffects 1.15
import Qt5Compat.GraphicalEffects
import Style 1.0
import "./tray"

View File

@ -1,18 +1,10 @@
project(gui)
find_package(Qt5 REQUIRED COMPONENTS Widgets Svg Qml Quick QuickControls2 QuickWidgets Xml Network)
find_package(KF5Archive REQUIRED)
find_package(KF5GuiAddons)
find_package(Qt${QT_MAJOR_VERSION} REQUIRED COMPONENTS Widgets Svg Qml Quick QuickControls2 QuickWidgets Xml Network)
find_package(KF6Archive REQUIRED)
find_package(KF6GuiAddons)
if(QUICK_COMPILER)
find_package(Qt5QuickCompiler)
set_package_properties(Qt5QuickCompiler PROPERTIES
DESCRIPTION "Compile QML at build time"
TYPE REQUIRED
)
endif()
if (NOT TARGET Qt5::GuiPrivate)
message(FATAL_ERROR "Could not find GuiPrivate component of Qt5. It might be shipped as a separate package, please check that.")
if (NOT TARGET Qt::GuiPrivate)
message(FATAL_ERROR "Could not find GuiPrivate component of Qt. It might be shipped as a separate package, please check that.")
endif()
if(CMAKE_BUILD_TYPE MATCHES Debug)
@ -55,11 +47,7 @@ set(client_UI_SRCS
wizard/welcomepage.ui
)
if(QUICK_COMPILER)
qtquick_compiler_add_resources(client_UI_SRCS ../../resources.qrc ${CMAKE_SOURCE_DIR}/theme.qrc)
else()
qt_add_resources(client_UI_SRCS ../../resources.qrc ${CMAKE_SOURCE_DIR}/theme.qrc)
endif()
qt_add_resources(client_UI_SRCS ../../resources.qrc ${CMAKE_SOURCE_DIR}/theme.qrc)
set(client_SRCS
accountmanager.h
@ -261,7 +249,7 @@ set(client_SRCS
wizard/linklabel.cpp
)
if (Qt5WebEngine_FOUND AND Qt5WebEngineWidgets_FOUND)
if (Qt${QT_MAJOR_VERSION}WebEngine_FOUND AND Qt${QT_MAJOR_VERSION}WebEngineWidgets_FOUND)
list(APPEND client_SRCS
wizard/webviewpage.h
wizard/webviewpage.cpp
@ -332,7 +320,7 @@ IF( APPLE )
list(APPEND updater_DEPS ${SPARKLE_LIBRARY})
# Sparkle.framework is installed from here because macdeployqt's CopyFramework breaks on this bundle
# as its logic is tightly tailored around Qt5 frameworks
# as its logic is tightly tailored around Qt frameworks
install(DIRECTORY "${SPARKLE_LIBRARY}"
DESTINATION "${OWNCLOUD_OSX_BUNDLE}/Contents/Frameworks" USE_SOURCE_PERMISSIONS)
@ -377,9 +365,9 @@ else()
set_property(SOURCE ../3rdparty/qtlockedfile/qtlockedfile_win.cpp PROPERTY SKIP_UNITY_BUILD_INCLUSION ON)
endif()
find_package(Qt5LinguistTools)
if(Qt5LinguistTools_FOUND)
qt5_add_translation(client_I18N ${TRANSLATIONS})
find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} COMPONENTS LinguistTools)
if(Qt${QT_MAJOR_VERSION}LinguistTools_FOUND)
qt_add_translation(client_I18N ${TRANSLATIONS})
endif()
IF( WIN32 )
@ -402,8 +390,8 @@ set( final_src
${3rdparty_MOC}
)
if(Qt5Keychain_FOUND)
list(APPEND libsync_LINK_TARGETS qt5keychain)
if(Qt${QT_MAJOR_VERSION}Keychain_FOUND)
list(APPEND libsync_LINK_TARGETS Qt6::keychain)
endif()
# add executable icon on windows and osx
@ -556,22 +544,22 @@ add_library(nextcloudCore STATIC ${final_src})
target_link_libraries(nextcloudCore
PUBLIC
Nextcloud::sync
Qt5::Widgets
Qt5::GuiPrivate
Qt5::Svg
Qt5::Network
Qt5::Xml
Qt5::Qml
Qt5::Quick
Qt5::QuickControls2
Qt5::QuickWidgets
KF5::Archive
Qt::Widgets
Qt::GuiPrivate
Qt::Svg
Qt::Network
Qt::Xml
Qt::Qml
Qt::Quick
Qt::QuickControls2
Qt::QuickWidgets
KF6::Archive
)
if(KF5GuiAddons_FOUND)
if(KF6GuiAddons_FOUND)
target_link_libraries(nextcloudCore
PUBLIC
KF5::GuiAddons
KF6::GuiAddons
)
add_definitions(-DHAVE_KGUIADDONS)
endif()
@ -588,8 +576,8 @@ foreach(FILE IN LISTS client_UI_SRCS)
set_property(SOURCE ${FILE} PROPERTY SKIP_UNITY_BUILD_INCLUSION ON)
endforeach()
if(Qt5WebEngine_FOUND AND Qt5WebEngineWidgets_FOUND)
target_link_libraries(nextcloudCore PUBLIC Qt5::WebEngineWidgets)
if(Qt6WebEngine_FOUND AND Qt6WebEngineWidgets_FOUND)
target_link_libraries(nextcloudCore PUBLIC Qt::WebEngineWidgets)
endif()
set_target_properties(nextcloudCore
@ -647,7 +635,7 @@ else()
set (QM_DIR ${OWNCLOUD_OSX_BUNDLE}/Contents/Resources/Translations)
install(FILES ${client_I18N} DESTINATION ${QM_DIR})
get_target_property(_qmake Qt5::qmake LOCATION)
get_target_property(_qmake Qt::qmake LOCATION)
execute_process(COMMAND ${_qmake} -query QT_INSTALL_TRANSLATIONS
OUTPUT_VARIABLE QT_TRANSLATIONS_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
@ -662,7 +650,7 @@ endif()
IF(BUILD_UPDATER)
add_library(updater STATIC ${updater_SRCS})
target_link_libraries(updater Nextcloud::sync ${updater_DEPS} Qt5::Widgets Qt5::Svg Qt5::Network Qt5::Xml)
target_link_libraries(updater Nextcloud::sync ${updater_DEPS} Qt::Widgets Qt::Svg Qt::Network Qt::Xml)
target_include_directories(updater PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(updater PROPERTIES AUTOMOC ON)
target_link_libraries(nextcloudCore PUBLIC updater)
@ -705,20 +693,16 @@ endif()
## handle DBUS for Fdo notifications
if( UNIX AND NOT APPLE )
find_package(Qt5 COMPONENTS DBus)
target_link_libraries(nextcloudCore PUBLIC Qt5::DBus)
find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} COMPONENTS DBus)
target_link_libraries(nextcloudCore PUBLIC Qt::DBus)
target_compile_definitions(nextcloudCore PUBLIC "USE_FDO_NOTIFICATIONS")
endif()
if (APPLE)
find_package(Qt5 COMPONENTS MacExtras)
if (BUILD_FILE_PROVIDER_MODULE)
target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras "-framework UserNotifications -framework FileProvider")
target_link_libraries(nextcloudCore PUBLIC "-framework UserNotifications -framework FileProvider")
elseif(CMAKE_OSX_DEPLOYMENT_TARGET VERSION_GREATER_EQUAL 10.14)
target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras "-framework UserNotifications")
else()
target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras)
target_link_libraries(nextcloudCore PUBLIC "-framework UserNotifications")
endif()
endif()
@ -746,7 +730,7 @@ install(TARGETS nextcloud
#
# OSX: Run macdeployqt for src/gui and for src/cmd using the -executable option
if(BUILD_OWNCLOUD_OSX_BUNDLE AND NOT BUILD_LIBRARIES_ONLY)
get_target_property (QT_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION)
get_target_property (QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION)
get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY)
find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${QT_BIN_DIR}")

View File

@ -31,6 +31,24 @@ Item {
CheckBox {
id: selectItem
palette {
text: Style.ncTextColor
windowText: Style.ncTextColor
buttonText: Style.ncTextColor
brightText: Style.ncTextBrightColor
highlight: Style.lightHover
highlightedText: Style.ncTextColor
light: Style.lightHover
midlight: Style.ncSecondaryTextColor
mid: Style.darkerHover
dark: Style.menuBorder
button: Style.buttonBackgroundColor
window: palette.dark // NOTE: Fusion theme uses darker window colour for the border of the checkbox
base: Style.backgroundColor
toolTipBase: Style.backgroundColor
toolTipText: Style.ncTextColor
}
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter

View File

@ -94,6 +94,24 @@ ApplicationWindow {
Layout.fillWidth: true
palette {
text: Style.ncTextColor
windowText: Style.ncTextColor
buttonText: Style.ncTextColor
brightText: Style.ncTextBrightColor
highlight: Style.lightHover
highlightedText: Style.ncTextColor
light: Style.lightHover
midlight: Style.ncSecondaryTextColor
mid: Style.darkerHover
dark: Style.menuBorder
button: Style.buttonBackgroundColor
window: palette.dark // NOTE: Fusion theme uses darker window colour for the border of the checkbox
base: Style.backgroundColor
toolTipBase: Style.backgroundColor
toolTipText: Style.ncTextColor
}
text: qsTr('All local versions')
leftPadding: 0
@ -112,6 +130,24 @@ ApplicationWindow {
Layout.fillWidth: true
palette {
text: Style.ncTextColor
windowText: Style.ncTextColor
buttonText: Style.ncTextColor
brightText: Style.ncTextBrightColor
highlight: Style.lightHover
highlightedText: Style.ncTextColor
light: Style.lightHover
midlight: Style.ncSecondaryTextColor
mid: Style.darkerHover
dark: Style.menuBorder
button: Style.buttonBackgroundColor
window: palette.dark // NOTE: Fusion theme uses darker window colour for the border of the checkbox
base: Style.backgroundColor
toolTipBase: Style.backgroundColor
toolTipText: Style.ncTextColor
}
text: qsTr('All server versions')
leftPadding: 0

View File

@ -13,6 +13,7 @@
*/
import QtQuick 2.6
import QtQuick.Dialogs
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
@ -53,36 +54,36 @@ ColumnLayout {
}
UserStatusSelectorButton {
checked: userStatusSelectorModel.onlineStatus === NC.UserStatus.Online
checked: userStatusSelectorModel.onlineStatus === NC.userStatus.Online
checkable: true
icon.source: userStatusSelectorModel.onlineIcon
icon.color: "transparent"
text: qsTr("Online")
onClicked: userStatusSelectorModel.onlineStatus = NC.UserStatus.Online
onClicked: userStatusSelectorModel.onlineStatus = NC.userStatus.Online
Layout.fillWidth: true
implicitWidth: 200 // Pretty much a hack to ensure all the buttons are equal in width
}
UserStatusSelectorButton {
checked: userStatusSelectorModel.onlineStatus === NC.UserStatus.Away
checked: userStatusSelectorModel.onlineStatus === NC.userStatus.Away
checkable: true
icon.source: userStatusSelectorModel.awayIcon
icon.color: "transparent"
text: qsTr("Away")
onClicked: userStatusSelectorModel.onlineStatus = NC.UserStatus.Away
onClicked: userStatusSelectorModel.onlineStatus = NC.userStatus.Away
Layout.fillWidth: true
implicitWidth: 200 // Pretty much a hack to ensure all the buttons are equal in width
}
UserStatusSelectorButton {
checked: userStatusSelectorModel.onlineStatus === NC.UserStatus.DoNotDisturb
checked: userStatusSelectorModel.onlineStatus === NC.userStatus.DoNotDisturb
checkable: true
icon.source: userStatusSelectorModel.dndIcon
icon.color: "transparent"
text: qsTr("Do not disturb")
secondaryText: qsTr("Mute all notifications")
onClicked: userStatusSelectorModel.onlineStatus = NC.UserStatus.DoNotDisturb
onClicked: userStatusSelectorModel.onlineStatus = NC.userStatus.DoNotDisturb
Layout.fillWidth: true
implicitWidth: 200 // Pretty much a hack to ensure all the buttons are equal in width
@ -91,14 +92,14 @@ ColumnLayout {
Component.onCompleted: topButtonsLayout.updateMaxButtonHeight(implicitHeight)
}
UserStatusSelectorButton {
checked: userStatusSelectorModel.onlineStatus === NC.UserStatus.Invisible ||
userStatusSelectorModel.onlineStatus === NC.UserStatus.Offline
checked: userStatusSelectorModel.onlineStatus === NC.userStatus.Invisible ||
userStatusSelectorModel.onlineStatus === NC.userStatus.Offline
checkable: true
icon.source: userStatusSelectorModel.invisibleIcon
icon.color: "transparent"
text: qsTr("Invisible")
secondaryText: qsTr("Appear offline")
onClicked: userStatusSelectorModel.onlineStatus = NC.UserStatus.Invisible
onClicked: userStatusSelectorModel.onlineStatus = NC.userStatus.Invisible
Layout.fillWidth: true
implicitWidth: 200 // Pretty much a hack to ensure all the buttons are equal in width

View File

@ -204,7 +204,6 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
const auto fpAccountUserIdAtHost = _accountState->account()->userIdAtHostWithPort();
const auto fpSettingsController = Mac::FileProviderSettingsController::instance();
const auto fpSettingsWidget = fpSettingsController->settingsViewWidget(fpAccountUserIdAtHost, fileProviderTab);
fpSettingsLayout->setMargin(0);
fpSettingsLayout->addWidget(fpSettingsWidget);
fileProviderTab->setLayout(fpSettingsLayout);
} else {
@ -244,7 +243,7 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
addAction(syncNowAction);
auto *syncNowWithRemoteDiscovery = new QAction(this);
syncNowWithRemoteDiscovery->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_F6));
syncNowWithRemoteDiscovery->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_F6));
connect(syncNowWithRemoteDiscovery, &QAction::triggered, this, &AccountSettings::slotScheduleCurrentFolderForceRemoteDiscovery);
addAction(syncNowWithRemoteDiscovery);

View File

@ -13,6 +13,7 @@
*/
#include "accountstate.h"
#include "accountmanager.h"
#include "remotewipe.h"
#include "account.h"
@ -23,6 +24,7 @@
#include "ocsnavigationappsjob.h"
#include "ocsuserstatusconnector.h"
#include "pushnotifications.h"
#include "networkjobs.h"
#include <QSettings>
#include <QTimer>
@ -32,6 +34,7 @@
#include <QJsonArray>
#include <QNetworkRequest>
#include <QBuffer>
#include <QRandomGenerator>
#include <cmath>
@ -44,7 +47,8 @@ AccountState::AccountState(AccountPtr account)
, _account(account)
, _state(AccountState::Disconnected)
, _connectionStatus(ConnectionValidator::Undefined)
, _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay
, _waitingForNewCredentials(false)
, _maintenanceToConnectedDelay(60000 + (QRandomGenerator::global()->generate() % (4 * 60000))) // 1-5min delay
, _remoteWipe(new RemoteWipe(_account))
, _isDesktopNotificationsAllowed(true)
{
@ -319,7 +323,7 @@ void AccountState::checkConnectivity()
// If we don't reset the ssl config a second CheckServerJob can produce a
// ssl config that does not have a sensible certificate chain.
account()->setSslConfiguration(QSslConfiguration());
account()->setSslConfiguration(QSslConfiguration::defaultConfiguration());
//#endif
conValidator->checkServerAndAuth();
}

View File

@ -66,6 +66,8 @@
#include <QGuiApplication>
#include <QUrlQuery>
#include <QVersionNumber>
#include <QRandomGenerator>
#include <QHttp2Configuration>
class QSocket;
@ -127,7 +129,7 @@ namespace {
#ifdef Q_OS_WIN
class WindowsNativeEventFilter : public QAbstractNativeEventFilter {
public:
bool nativeEventFilter(const QByteArray &eventType, void *message, long *result) override {
bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override {
const auto msg = static_cast<MSG *>(message);
if(msg->message == WM_SYSCOLORCHANGE || msg->message == WM_SETTINGCHANGE) {
if (const auto ptr = qobject_cast<QGuiApplication *>(QGuiApplication::instance())) {
@ -228,8 +230,6 @@ Application::Application(int &argc, char **argv)
{
_startedAt.start();
qsrand(std::random_device()());
#ifdef Q_OS_WIN
// Ensure OpenSSL config file is only loaded from app directory
QString opensslConf = QCoreApplication::applicationDirPath() + QString("/openssl.cnf");
@ -254,7 +254,45 @@ Application::Application(int &argc, char **argv)
setApplicationName(_theme->appName());
setWindowIcon(_theme->applicationIcon());
if (ConfigFile().exists()) {
if (!ConfigFile().exists()) {
// Migrate from version <= 2.4
setApplicationName(_theme->appNameGUI());
// We need to use the deprecated QDesktopServices::storageLocation because of its Qt4
// behavior of adding "data" to the path
QString oldDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/data/" + organizationName() + "/" + applicationName();
if (oldDir.endsWith('/')) oldDir.chop(1); // macOS 10.11.x does not like trailing slash for rename/move.
setApplicationName(_theme->appName());
if (QFileInfo(oldDir).isDir()) {
auto confDir = ConfigFile().configPath();
// macOS 10.11.x does not like trailing slash for rename/move.
if (confDir.endsWith('/')) {
confDir.chop(1);
}
qCInfo(lcApplication) << "Migrating old config from" << oldDir << "to" << confDir;
if (!QFile::rename(oldDir, confDir)) {
qCWarning(lcApplication) << "Failed to move the old config directory to its new location (" << oldDir << "to" << confDir << ")";
// Try to move the files one by one
if (QFileInfo(confDir).isDir() || QDir().mkdir(confDir)) {
const QStringList filesList = QDir(oldDir).entryList(QDir::Files);
qCInfo(lcApplication) << "Will move the individual files" << filesList;
for (const auto &name : filesList) {
if (!QFile::rename(oldDir + "/" + name, confDir + "/" + name)) {
qCWarning(lcApplication) << "Fallback move of " << name << "also failed";
}
}
}
} else {
#ifndef Q_OS_WIN
// Create a symbolic link so a downgrade of the client would still find the config.
QFile::link(confDir, oldDir);
#endif
}
}
} else {
setupConfigFile();
}
@ -390,7 +428,7 @@ Application::Application(int &argc, char **argv)
QTimer::singleShot(0, this, &Application::slotCheckConnection);
// Can't use onlineStateChanged because it is always true on modern systems because of many interfaces
connect(&_networkConfigurationManager, &QNetworkConfigurationManager::configurationChanged,
connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged,
this, &Application::slotSystemOnlineConfigurationChanged);
#if defined(BUILD_UPDATER)
@ -492,7 +530,7 @@ void Application::setupConfigFile()
QT_WARNING_POP
setApplicationName(_theme->appName());
auto oldDir = QStandardPaths::writableLocation(QStandardPaths::DataLocation);
auto oldDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
// macOS 10.11.x does not like trailing slash for rename/move.
if (oldDir.endsWith('/')) {
@ -608,9 +646,10 @@ void Application::slotCleanup()
// FIXME: This is not ideal yet since a ConnectionValidator might already be running and is in
// progress of timing out in some seconds.
// Maybe we need 2 validators, one triggered by timer, one by network configuration changes?
void Application::slotSystemOnlineConfigurationChanged(QNetworkConfiguration cnf)
void Application::slotSystemOnlineConfigurationChanged()
{
if (cnf.state() & QNetworkConfiguration::Active) {
if (QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Site ||
QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Online) {
const auto list = AccountManager::instance()->accounts();
for (const auto &accountState : list) {
accountState->systemOnlineConfigurationChanged();
@ -660,8 +699,13 @@ void Application::setupLogging()
logger->setLogDir(_logDir.isEmpty() ? ConfigFile().logDir() : _logDir);
}
logger->setLogExpire(_logExpire > 0 ? _logExpire : ConfigFile().logExpire());
#if defined NEXTCLOUD_DEV
logger->setLogFlush(true);
logger->setLogDebug(true);
#else
logger->setLogFlush(_logFlush || ConfigFile().logFlush());
logger->setLogDebug(_logDebug || ConfigFile().logDebug());
#endif
if (!logger->isLoggingToFile() && ConfigFile().automaticLogDir()) {
logger->setupTemporaryFolderLogDir();
}
@ -866,16 +910,16 @@ void Application::showHelp()
QTextStream stream(&helpText);
stream << _theme->appName()
<< QLatin1String(" version ")
<< _theme->version() << endl;
<< _theme->version() << Qt::endl;
stream << QLatin1String("File synchronisation desktop utility.") << endl
<< endl
stream << QLatin1String("File synchronisation desktop utility.") << Qt::endl
<< Qt::endl
<< QLatin1String(optionsC);
if (_theme->appName() == QLatin1String("ownCloud"))
stream << endl
<< "For more information, see http://www.owncloud.org" << endl
<< endl;
stream << Qt::endl
<< "For more information, see http://www.owncloud.org" << Qt::endl
<< Qt::endl;
displayHelpText(helpText);
}
@ -980,13 +1024,17 @@ void Application::setupTranslations()
if (!qtTranslator->load(qtTrFile, qtTrPath)) {
if (!qtTranslator->load(qtTrFile, trPath)) {
if (!qtTranslator->load(qtBaseTrFile, qtTrPath)) {
qtTranslator->load(qtBaseTrFile, trPath);
if (!qtTranslator->load(qtBaseTrFile, trPath)) {
qCDebug(lcApplication()) << "impossible to load Qt translation catalog" << qtBaseTrFile;
}
}
}
}
const QString qtkeychainTrFile = QLatin1String("qtkeychain_") + lang;
if (!qtkeychainTranslator->load(qtkeychainTrFile, qtTrPath)) {
qtkeychainTranslator->load(qtkeychainTrFile, trPath);
if (!qtkeychainTranslator->load(qtkeychainTrFile, trPath)) {
qCDebug(lcApplication()) << "impossible to load QtKeychain translation catalog" << qtkeychainTrFile;
}
}
if (!translator->isEmpty())
installTranslator(translator);

View File

@ -20,7 +20,7 @@
#include <QQueue>
#include <QTimer>
#include <QElapsedTimer>
#include <QNetworkConfigurationManager>
#include <QNetworkInformation>
#include "qtsingleapplication.h"
@ -110,7 +110,7 @@ protected slots:
void slotCleanup();
void slotAccountStateAdded(OCC::AccountState *accountState);
void slotAccountStateRemoved(OCC::AccountState *accountState);
void slotSystemOnlineConfigurationChanged(QNetworkConfiguration);
void slotSystemOnlineConfigurationChanged();
void slotGuiIsShowingSettings();
private:
@ -152,7 +152,6 @@ private:
ClientProxy _proxy;
QNetworkConfigurationManager _networkConfigurationManager;
QTimer _checkConnectionTimer;
QString _overrideServerUrl;

View File

@ -2,6 +2,15 @@
#include "creds/httpcredentials.h"
#include "creds/keychainchunk.h"
#include "accessmanager.h"
#include "account.h"
#include "configfile.h"
#include "theme.h"
#ifdef WITH_WEBENGINE
#include "wizard/webview.h"
#endif // WITH_WEBENGINE
#include "webflowcredentialsdialog.h"
#include "networkjobs.h"
#include <QAuthenticator>
#include <QNetworkAccessManager>
@ -12,15 +21,6 @@
#include <QVBoxLayout>
#include <QLabel>
#include "accessmanager.h"
#include "account.h"
#include "configfile.h"
#include "theme.h"
#ifdef WITH_WEBENGINE
#include "wizard/webview.h"
#endif // WITH_WEBENGINE
#include "webflowcredentialsdialog.h"
using namespace QKeychain;
namespace OCC {

View File

@ -22,13 +22,13 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlo
_layout = new QVBoxLayout(this);
int spacing = _layout->spacing();
int margin = _layout->margin();
auto margin = _layout->contentsMargins();
_layout->setSpacing(0);
_layout->setMargin(0);
_layout->setContentsMargins(0, 0, 0, 0);
_containerLayout = new QVBoxLayout(this);
_containerLayout->setSpacing(spacing);
_containerLayout->setMargin(margin);
_containerLayout->setContentsMargins(margin);
_infoLabel = new QLabel();
_infoLabel->setTextFormat(Qt::PlainText);

View File

@ -586,7 +586,7 @@ void EditLocallyJob::openFile()
// In case the VFS mode is enabled and a file is not yet hydrated, we must call QDesktopServices::openUrl
// from a separate thread, or, there will be a freeze. To avoid searching for a specific folder and checking
// if the VFS is enabled - we just always call it from a separate thread.
QtConcurrent::run([localFilePathUrl, this]() {
auto futureResult = QtConcurrent::run([localFilePathUrl, this]() {
if (!QDesktopServices::openUrl(localFilePathUrl)) {
emit callShowError(tr("Could not open %1").arg(_fileName), tr("Please try again."));
}

View File

@ -15,6 +15,7 @@
#include "editlocallymanager.h"
#include <QUrl>
#include <QUrlQuery>
#include <QLoggingCategory>
namespace OCC {

View File

@ -16,7 +16,7 @@ import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtGraphicalEffects 1.15
import Qt5Compat.GraphicalEffects
import com.nextcloud.desktopclient 1.0
import Style 1.0

View File

@ -16,7 +16,7 @@ import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtGraphicalEffects 1.15
import Qt5Compat.GraphicalEffects
import com.nextcloud.desktopclient 1.0
import Style 1.0

View File

@ -33,6 +33,7 @@ namespace {
static const auto placeholderLinkShareId = QStringLiteral("__placeholderLinkShareId__");
static const auto internalLinkShareId = QStringLiteral("__internalLinkShareId__");
static const auto secureFileDropPlaceholderLinkShareId = QStringLiteral("__secureFileDropPlaceholderLinkShareId__");
}
namespace OCC
@ -1363,7 +1364,7 @@ QString ShareModel::generatePassword()
for (const auto newChar : unsignedCharArray) {
// Ensure byte is within asciiRange
const auto byte = (newChar % (asciiRange + 1)) + asciiMin;
const auto byte = QChar((newChar % (asciiRange + 1)) + asciiMin);
passwd.append(byte);
}

View File

@ -557,17 +557,17 @@ int Folder::slotWipeErrorBlacklist()
return _journal.wipeErrorBlacklist();
}
void Folder::slotWatchedPathChanged(const QString &path, ChangeReason reason)
void Folder::slotWatchedPathChanged(const QStringView &path, const ChangeReason reason)
{
if (!path.startsWith(this->path())) {
qCDebug(lcFolder) << "Changed path is not contained in folder, ignoring:" << path;
return;
}
auto relativePath = path.midRef(this->path().size());
auto relativePath = path.mid(this->path().size());
if (_vfs) {
if (pathIsIgnored(path)) {
if (pathIsIgnored(path.toString())) {
const auto pinState = _vfs->pinState(relativePath.toString());
if (!pinState || *pinState != PinState::Excluded) {
if (!_vfs->setPinState(relativePath.toString(), PinState::Excluded)) {
@ -601,7 +601,7 @@ void Folder::slotWatchedPathChanged(const QString &path, ChangeReason reason)
// own process. Therefore nothing needs to be done here!
#else
// Use the path to figure out whether it was our own change
if (_engine->wasFileTouched(path)) {
if (_engine->wasFileTouched(path.toString())) {
qCDebug(lcFolder) << "Changed path was touched by SyncEngine, ignoring:" << path;
return;
}
@ -617,17 +617,22 @@ void Folder::slotWatchedPathChanged(const QString &path, ChangeReason reason)
// an attribute change (pin state) that caused the notification
bool spurious = false;
if (record.isValid()
&& !FileSystem::fileChanged(path, record._fileSize, record._modtime) && _vfs) {
&& !FileSystem::fileChanged(path.toString(), record._fileSize, record._modtime) && _vfs) {
spurious = true;
if (auto pinState = _vfs->pinState(relativePath.toString())) {
if (*pinState == PinState::AlwaysLocal && record.isVirtualFile())
if (*pinState == PinState::AlwaysLocal && record.isVirtualFile()) {
spurious = false;
if (*pinState == PinState::OnlineOnly && record.isFile())
}
if (*pinState == PinState::OnlineOnly && record.isFile()) {
spurious = false;
}
} else {
spurious = false;
}
if (spurious && !_vfs->isPlaceHolderInSync(path.toString())) {
spurious = false;
}
}
if (spurious) {
qCInfo(lcFolder) << "Ignoring spurious notification for file" << relativePath;
@ -636,7 +641,7 @@ void Folder::slotWatchedPathChanged(const QString &path, ChangeReason reason)
}
warnOnNewExcludedItem(record, relativePath);
emit watchedFileChangedExternally(path);
emit watchedFileChangedExternally(path.toString());
// Also schedule this folder for a sync, but only after some delay:
// The sync will not upload files that were changed too recently.
@ -1456,7 +1461,7 @@ void Folder::slotFolderConflicts(const QString &folder, const QStringList &confl
r.setNumOldConflictItems(conflictPaths.size() - r.numNewConflictItems());
}
void Folder::warnOnNewExcludedItem(const SyncJournalFileRecord &record, const QStringRef &path)
void Folder::warnOnNewExcludedItem(const SyncJournalFileRecord &record, const QStringView &path)
{
// Never warn for items in the database
if (record.isValid())
@ -1598,6 +1603,7 @@ void Folder::registerFolderWatcher()
connect(_folderWatcher.data(), &FolderWatcher::filesLockImposed, this, &Folder::slotFilesLockImposed, Qt::UniqueConnection);
_folderWatcher->init(path());
_folderWatcher->startNotificatonTest(path() + QLatin1String(".nextcloudsync.log"));
connect(_engine.data(), &SyncEngine::lockFileDetected, _folderWatcher.data(), &FolderWatcher::slotLockFileDetectedExternally);
}
void Folder::disconnectFolderWatcher()
@ -1707,8 +1713,8 @@ void Folder::removeLocalE2eFiles()
const auto existingBlacklist = _journal.getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
Q_ASSERT(ok);
const auto existingBlacklistSet = existingBlacklist.toSet();
auto expandedBlacklistSet = existingBlacklist.toSet();
const auto existingBlacklistSet = QSet<QString>{existingBlacklist.begin(), existingBlacklist.end()};
auto expandedBlacklistSet = QSet<QString>{existingBlacklist.begin(), existingBlacklist.end()};
for (const auto &path : qAsConst(e2eFoldersToBlacklist)) {
expandedBlacklistSet.insert(path);

View File

@ -26,6 +26,8 @@
#include <QObject>
#include <QStringList>
#include <QUuid>
#include <QSet>
#include <set>
#include <chrono>
#include <memory>
@ -350,7 +352,7 @@ public slots:
* changes. Needs to check whether this change should trigger a new
* sync run to be scheduled.
*/
void slotWatchedPathChanged(const QString &path, OCC::Folder::ChangeReason reason);
void slotWatchedPathChanged(const QStringView &path, const OCC::Folder::ChangeReason reason);
/*
* Triggered when lock files were removed
@ -441,7 +443,7 @@ private slots:
void slotFolderConflicts(const QString &folder, const QStringList &conflictPaths);
/** Warn users if they create a file or folder that is selective-sync excluded */
void warnOnNewExcludedItem(const OCC::SyncJournalFileRecord &record, const QStringRef &path);
void warnOnNewExcludedItem(const OCC::SyncJournalFileRecord &record, const QStringView &path);
/** Warn users about an unreliable folder watcher */
void slotWatcherUnreliable(const QString &message);

View File

@ -1319,7 +1319,7 @@ QStringList FolderMan::findFileInLocalFolders(const QString &relPath, const Acco
continue;
QString path = folder->cleanPath() + '/';
path += serverPath.midRef(folder->remotePathTrailingSlash().length());
path += serverPath.mid(folder->remotePathTrailingSlash().length());
if (QFile::exists(path)) {
re.append(path);
}

View File

@ -72,10 +72,8 @@ QSize FolderStatusDelegate::sizeHint(const QStyleOptionViewItem &option,
QStyleOptionButton opt;
static_cast<QStyleOption &>(opt) = option;
opt.text = addFolderText();
return QApplication::style()->sizeFromContents(
QStyle::CT_PushButton, &opt, fm.size(Qt::TextSingleLine, opt.text))
.expandedTo(QApplication::globalStrut())
+ QSize(0, margins);
return QApplication::style()->sizeFromContents( QStyle::CT_PushButton, &opt, fm.size(Qt::TextSingleLine, opt.text)) +
QSize(0, margins);
}
if (classif != FolderStatusModel::RootFolder) {
@ -314,7 +312,7 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
progressBarOpt.minimum = 0;
progressBarOpt.maximum = 100;
progressBarOpt.progress = overallPercent;
progressBarOpt.orientation = Qt::Horizontal;
progressBarOpt.state = QStyle::StateFlag::State_Horizontal;
progressBarOpt.rect = QStyle::visualRect(option.direction, option.rect, progressBarRect);
#ifdef Q_OS_MACOS
backupStyle->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget);
@ -322,7 +320,6 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget);
#endif
// Overall Progress Text
QRect overallProgressRect;
overallProgressRect.setTop(progressBarRect.bottom() + margin);
@ -390,7 +387,7 @@ QRect FolderStatusDelegate::optionsButtonRect(QRect within, Qt::LayoutDirection
QStyleOptionToolButton opt;
int e = QApplication::style()->pixelMetric(QStyle::PM_ButtonIconSize);
opt.rect.setSize(QSize(e,e));
QSize size = QApplication::style()->sizeFromContents(QStyle::CT_ToolButton, &opt, opt.rect.size()).expandedTo(QApplication::globalStrut());
QSize size = QApplication::style()->sizeFromContents(QStyle::CT_ToolButton, &opt, opt.rect.size());
int margin = QApplication::style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
QRect r(QPoint(within.right() - size.width() - margin,
@ -404,7 +401,7 @@ QRect FolderStatusDelegate::addButtonRect(QRect within, Qt::LayoutDirection dire
QFontMetrics fm(qApp->font("QPushButton"));
QStyleOptionButton opt;
opt.text = addFolderText();
QSize size = QApplication::style()->sizeFromContents(QStyle::CT_PushButton, &opt, fm.size(Qt::TextSingleLine, opt.text)).expandedTo(QApplication::globalStrut());
QSize size = QApplication::style()->sizeFromContents(QStyle::CT_PushButton, &opt, fm.size(Qt::TextSingleLine, opt.text));
QRect r(QPoint(within.left(), within.top() + within.height() / 2 - size.height() / 2), size);
return QStyle::visualRect(direction, within, r);
}

View File

@ -921,12 +921,13 @@ void FolderStatusModel::slotApplySelectiveSync()
const auto blackList = createBlackList(folderInfo, oldBlackList);
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList);
auto blackListSet = blackList.toSet();
auto oldBlackListSet = oldBlackList.toSet();
auto blackListSet = QSet<QString>{blackList.begin(), blackList.end()};
auto oldBlackListSet = QSet<QString>{oldBlackList.begin(), oldBlackList.end()};
// The folders that were undecided or blacklisted and that are now checked should go on the white list.
// The user confirmed them already just now.
const auto toAddToWhiteList = ((oldBlackListSet + folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &ok).toSet()) - blackListSet).values();
const auto selectiveSyncList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &ok);
const auto toAddToWhiteList = ((oldBlackListSet + QSet<QString>{selectiveSyncList.begin(), selectiveSyncList.end()}) - blackListSet).values();
if (!toAddToWhiteList.isEmpty()) {
auto whiteList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, &ok);

View File

@ -41,34 +41,7 @@
namespace
{
const std::array<const char *, 2> lockFilePatterns = {{".~lock.", "~$"}};
constexpr auto lockChangeDebouncingTimerIntervalMs = 500;
QString filePathLockFilePatternMatch(const QString &path)
{
qCDebug(OCC::lcFolderWatcher) << "Checking if it is a lock file:" << path;
const auto pathSplit = path.split(QLatin1Char('/'), Qt::SkipEmptyParts);
if (pathSplit.isEmpty()) {
return {};
}
QString lockFilePatternFound;
for (const auto &lockFilePattern : lockFilePatterns) {
if (pathSplit.last().startsWith(lockFilePattern)) {
lockFilePatternFound = lockFilePattern;
break;
}
}
if (lockFilePatternFound.isEmpty()) {
return {};
}
qCDebug(OCC::lcFolderWatcher) << "Found a lock file with prefix:" << lockFilePatternFound << "in path:" << path;
return lockFilePatternFound;
}
}
namespace OCC {
@ -185,6 +158,22 @@ int FolderWatcher::testLinuxWatchCount() const
#endif
}
void FolderWatcher::slotLockFileDetectedExternally(const QString &lockFile)
{
qCInfo(lcFolderWatcher) << "Lock file detected externally, probably a newly-uploaded office file: " << lockFile;
changeDetected(lockFile);
}
void FolderWatcher::setShouldWatchForFileUnlocking(bool shouldWatchForFileUnlocking)
{
_shouldWatchForFileUnlocking = shouldWatchForFileUnlocking;
}
int FolderWatcher::lockChangeDebouncingTimout() const
{
return _lockChangeDebouncingTimer.interval();
}
void FolderWatcher::changeDetected(const QString &path)
{
QFileInfo fileInfo(path);
@ -204,7 +193,7 @@ void FolderWatcher::changeDetected(const QStringList &paths)
// - why do we skip the file altogether instead of e.g. reducing the upload frequency?
// Check if the same path was reported within the last second.
const auto pathsSet = paths.toSet();
const auto pathsSet = QSet<QString>{paths.begin(), paths.end()};
if (pathsSet == _lastPaths && _timer.elapsed() < 1000) {
// the same path was reported within the last second. Skip.
return;
@ -220,17 +209,17 @@ void FolderWatcher::changeDetected(const QStringList &paths)
_testNotificationPath.clear();
}
const auto lockFileNamePattern = filePathLockFilePatternMatch(path);
const auto checkResult = lockFileTargetFilePath(path,lockFileNamePattern);
const auto lockFileNamePattern = FileSystem::filePathLockFilePatternMatch(path);
const auto checkResult = FileSystem::lockFileTargetFilePath(path, lockFileNamePattern);
if (_shouldWatchForFileUnlocking) {
// Lock file has been deleted, file now unlocked
if (checkResult.type == FileLockingInfo::Type::Unlocked && !checkResult.path.isEmpty()) {
if (checkResult.type == FileSystem::FileLockingInfo::Type::Unlocked && !checkResult.path.isEmpty()) {
_lockedFiles.remove(checkResult.path);
_unlockedFiles.insert(checkResult.path);
}
}
if (checkResult.type == FileLockingInfo::Type::Locked && !checkResult.path.isEmpty()) {
if (checkResult.type == FileSystem::FileLockingInfo::Type::Locked && !checkResult.path.isEmpty()) {
_unlockedFiles.remove(checkResult.path);
_lockedFiles.insert(checkResult.path);
}
@ -272,62 +261,4 @@ void FolderWatcher::folderAccountCapabilitiesChanged()
_shouldWatchForFileUnlocking = _folder->accountState()->account()->capabilities().filesLockAvailable();
}
FolderWatcher::FileLockingInfo FolderWatcher::lockFileTargetFilePath(const QString &path, const QString &lockFileNamePattern) const
{
FileLockingInfo result;
if (lockFileNamePattern.isEmpty()) {
return result;
}
const auto lockFilePathWithoutPrefix = QString(path).replace(lockFileNamePattern, QStringLiteral(""));
auto lockFilePathWithoutPrefixSplit = lockFilePathWithoutPrefix.split(QLatin1Char('.'));
if (lockFilePathWithoutPrefixSplit.size() < 2) {
return result;
}
auto extensionSanitized = lockFilePathWithoutPrefixSplit.takeLast().toStdString();
// remove possible non-alphabetical characters at the end of the extension
extensionSanitized.erase(
std::remove_if(extensionSanitized.begin(), extensionSanitized.end(), [](const auto &ch) {
return !std::isalnum(ch);
}),
extensionSanitized.end()
);
lockFilePathWithoutPrefixSplit.push_back(QString::fromStdString(extensionSanitized));
const auto lockFilePathWithoutPrefixNew = lockFilePathWithoutPrefixSplit.join(QLatin1Char('.'));
qCDebug(lcFolderWatcher) << "Assumed locked/unlocked file path" << lockFilePathWithoutPrefixNew << "Going to try to find matching file";
auto splitFilePath = lockFilePathWithoutPrefixNew.split(QLatin1Char('/'));
if (splitFilePath.size() > 1) {
const auto lockFileNameWithoutPrefix = splitFilePath.takeLast();
// some software will modify lock file name such that it does not correspond to original file (removing some symbols from the name, so we will search
// for a matching file
result.path = findMatchingUnlockedFileInDir(splitFilePath.join(QLatin1Char('/')), lockFileNameWithoutPrefix);
}
if (result.path.isEmpty() || !QFile::exists(result.path)) {
result.path.clear();
return result;
}
result.type = QFile::exists(path) ? FileLockingInfo::Type::Locked : FileLockingInfo::Type::Unlocked;
return result;
}
QString FolderWatcher::findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName) const
{
QString foundFilePath;
const QDir dir(dirPath);
const auto entryList = dir.entryInfoList(QDir::Files);
for (const auto &candidateUnlockedFileInfo : entryList) {
if (candidateUnlockedFileInfo.fileName().contains(lockFileName)) {
foundFilePath = candidateUnlockedFileInfo.absoluteFilePath();
break;
}
}
return foundFilePath;
}
} // namespace OCC

View File

@ -50,12 +50,6 @@ class FolderWatcher : public QObject
{
Q_OBJECT
struct FileLockingInfo {
enum class Type { Unset = -1, Locked, Unlocked };
QString path;
Type type = Type::Unset;
};
public:
// Construct, connect signals, call init()
explicit FolderWatcher(Folder *folder = nullptr);
@ -86,6 +80,11 @@ public:
/// For testing linux behavior only
[[nodiscard]] int testLinuxWatchCount() const;
void slotLockFileDetectedExternally(const QString &lockFile);
void setShouldWatchForFileUnlocking(bool shouldWatchForFileUnlocking);
[[nodiscard]] int lockChangeDebouncingTimout() const;
signals:
/** Emitted when one of the watched directories or one
* of the contained files is changed. */
@ -101,8 +100,6 @@ signals:
*/
void filesLockImposed(const QSet<QString> &files);
void lockFilesFound(const QSet<QString> &files);
void lockedFilesFound(const QSet<QString> &files);
/**
@ -145,11 +142,6 @@ private:
void appendSubPaths(QDir dir, QStringList& subPaths);
[[nodiscard]] FileLockingInfo lockFileTargetFilePath(const QString &path, const QString &lockFileNamePattern) const;
[[nodiscard]] QString findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName) const;
QString findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName);
/* Check if the path should be ignored by the FolderWatcher. */
[[nodiscard]] bool pathIsIgnored(const QString &path) const;

View File

@ -38,6 +38,7 @@
#include <QEvent>
#include <QCheckBox>
#include <QMessageBox>
#include <QStandardPaths>
#include <cstdlib>

Some files were not shown because too many files have changed in this diff Show More