diff --git a/CHANGELOG.md b/CHANGELOG.md index ae23091d1..2d071e6f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # ChangeLog +## Release version 1.2.1 (January 2020) + +- Fix: Passcode Lock Screen on iOS 13 (#582) + +## Release version 1.2.0 (December 2019) + +- Multiple Window Support (iPadOS) (#488) +- Keyboard Commands (iPadOS) (#282) +- Media Player Improvements (#59, #374) +- Better File Previews (#481) +- Arabic Language Support +- Fix: Sort alphabetically (PR #546) +- Fix: Share Sheet on iPad (#568) +- Fix: FileProvider File Type Issue (#557) +- Fix: FileProvider Offline Browsing (PR #547) +- Fix: FileProvider Saving from Microsoft Word (PR #574) +- Fix: Photo Upload (#504) + ## Release version 1.1.2 (October 2019) - Fix for long delays before starting a request on iOS 13.1 (PR #531) diff --git a/docs/yarn.lock b/docs/yarn.lock index 499de6493..ea742b059 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -482,11 +482,16 @@ combined-stream2@^1.0.2: debug "^2.1.1" stream-length "^1.0.1" -commander@^2.12.2, commander@^2.19.0, commander@~2.20.0: +commander@^2.12.2, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -897,9 +902,9 @@ gulp-vinyl-zip@^2.1.2: yazl "^2.2.1" handlebars@^4.0.12: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + version "4.5.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" + integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -1960,11 +1965,11 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= uglify-js@^3.1.4: - version "3.5.15" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.15.tgz#fe2b5378fd0b09e116864041437bff889105ce24" - integrity sha512-fe7aYFotptIddkwcm6YuA0HmknBZ52ZzOsUxZEdhhkSsz7RfjHDX2QDxwKTiv4JQ5t5NhfmpgAK+J7LiDhKSqg== + version "3.7.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a" + integrity sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg== dependencies: - commander "~2.20.0" + commander "~2.20.3" source-map "~0.6.1" unc-path-regex@^0.1.2: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 557ce862d..1f3bca0db 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -37,7 +37,8 @@ platform :ios do provisioningProfiles: { "com.owncloud.ios-app" => "match AdHoc com.owncloud.ios-app", "com.owncloud.ios-app.ownCloud-File-Provider" => "match AdHoc com.owncloud.ios-app.ownCloud-File-Provider", - "com.owncloud.ios-app.ownCloud-File-ProviderUI" => "match AdHoc com.owncloud.ios-app.ownCloud-File-ProviderUI" + "com.owncloud.ios-app.ownCloud-File-ProviderUI" => "match AdHoc com.owncloud.ios-app.ownCloud-File-ProviderUI", + "com.owncloud.ios-app.ownCloud-File-Intents" => "match AdHoc com.owncloud.ios-app.ownCloud-Intents" #Add more Provisioning Profiles when extensions are added } } @@ -71,6 +72,8 @@ platform :ios do ENTERPRISE_FP_PROFILE = "Enterprise Distribution iOS Neo File Provider" ENTERPRISE_FP_UI_ID = "com.owncloud.enterprise.ios-app.ownCloud-File-ProviderUI" ENTERPRISE_FP_UI_PROFILE = "Enterprise Distribution iOS Neo File ProviderUI" + ENTERPRISE_INTENT_ID = "com.owncloud.enterprise.ios-app.ownCloud-Intents" + ENTERPRISE_INTENT_PROFILE = "Enterprise Distribution iOS Neo Intent" ENTERPRISE_APP_FW_ID = "com.owncloud.enterprise.ownCloudApp" ENTERPRISE_TEAM = "5QNK8L2PSC" ENTERPRISE_IDENTITY = "iPhone Distribution: ownCloud GmbH" @@ -99,6 +102,12 @@ platform :ios do app_identifier: ENTERPRISE_APP_FW_ID ) + update_app_identifier( + xcodeproj: "ownCloud.xcodeproj", + plist_path: "ownCloud Intents/Info.plist", + app_identifier: ENTERPRISE_INTENT_ID + ) + update_app_group_identifiers( entitlements_file: "ownCloud/ownCloud.entitlements", app_group_identifiers: ["group.com.owncloud.enterprise.ios-app"] @@ -109,10 +118,18 @@ platform :ios do app_group_identifiers: ["group.com.owncloud.enterprise.ios-app"] ) + update_app_group_identifiers( + entitlements_file: "ownCloud Intents/ownCloud Intents.entitlements", + app_group_identifiers: ["group.com.owncloud.enterprise.ios-app"] + ) + set_info_plist_value(path: "ownCloud File Provider/Info.plist", key: "OCAppGroupIdentifier", value: "group.com.owncloud.enterprise.ios-app") set_info_plist_value(path: "ownCloud File Provider/Info.plist", key: "OCKeychainAccessGroupIdentifier", value: "group.com.owncloud.enterprise.ios-app") set_info_plist_value(path: "ownCloud File Provider/Info.plist", key: "NSExtension", subkey: "NSExtensionFileProviderDocumentGroup", value: "group.com.owncloud.enterprise.ios-app") + set_info_plist_value(path: "ownCloud Intents/Info.plist", key: "OCAppGroupIdentifier", value: "group.com.owncloud.enterprise.ios-app") + set_info_plist_value(path: "ownCloud Intents/Info.plist", key: "OCKeychainAccessGroupIdentifier", value: "group.com.owncloud.enterprise.ios-app") + automatic_code_signing( path: "ownCloud.xcodeproj", use_automatic_signing: false, @@ -143,6 +160,16 @@ platform :ios do targets: ["ownCloud File ProviderUI"] ) + automatic_code_signing( + path: "ownCloud.xcodeproj", + use_automatic_signing: false, + team_id: ENTERPRISE_TEAM, + code_sign_identity: ENTERPRISE_IDENTITY, + profile_name: ENTERPRISE_INTENT_PROFILE, + bundle_identifier: ENTERPRISE_INTENT_ID, + targets: ["ownCloud Intents"] + ) + automatic_code_signing( path: "ownCloud.xcodeproj", use_automatic_signing: false, @@ -157,6 +184,9 @@ platform :ios do short_hash = commit[:abbreviated_commit_hash] # short sha of commit sh "curl https://badgen.net/badge/Bitrise/" + short_hash + "/blue > badge.svg" sh "brew install librsvg" + sh "brew unlink pango" + sh "brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/7cf3b63be191cb2ce4cd86f4406915128ec97432/Formula/pango.rb" + sh "brew switch pango 1.42.4_1 " sh "rsvg-convert badge.svg > badge.png" sh "badge --custom badge.png --glob /../**/*.appiconset/*.{png,PNG}" @@ -172,7 +202,8 @@ platform :ios do provisioningProfiles: { ENTERPRISE_APP_ID => ENTERPRISE_APP_PROFILE, ENTERPRISE_FP_ID => ENTERPRISE_FP_PROFILE, - ENTERPRISE_FP_UI_ID => ENTERPRISE_FP_UI_PROFILE + ENTERPRISE_FP_UI_ID => ENTERPRISE_FP_UI_PROFILE, + ENTERPRISE_INTENT_ID => ENTERPRISE_INTENT_PROFILE } } ) diff --git a/fastlane/screenshots/ar/keyword.strings b/fastlane/screenshots/ar/keyword.strings new file mode 100644 index 000000000..282ef33d1 Binary files /dev/null and b/fastlane/screenshots/ar/keyword.strings differ diff --git a/fastlane/screenshots/ar/title.strings b/fastlane/screenshots/ar/title.strings new file mode 100644 index 000000000..2639e5ebd Binary files /dev/null and b/fastlane/screenshots/ar/title.strings differ diff --git a/fastlane/screenshots/en-GB/keyword.strings b/fastlane/screenshots/en-GB/keyword.strings new file mode 100644 index 000000000..aba384182 Binary files /dev/null and b/fastlane/screenshots/en-GB/keyword.strings differ diff --git a/fastlane/screenshots/en-GB/title.strings b/fastlane/screenshots/en-GB/title.strings new file mode 100644 index 000000000..547033c70 Binary files /dev/null and b/fastlane/screenshots/en-GB/title.strings differ diff --git a/fastlane/screenshots/gl/keyword.strings b/fastlane/screenshots/gl/keyword.strings index 7cb3afe8d..db8cff283 100644 Binary files a/fastlane/screenshots/gl/keyword.strings and b/fastlane/screenshots/gl/keyword.strings differ diff --git a/fastlane/screenshots/lv/title.strings b/fastlane/screenshots/lv/title.strings new file mode 100644 index 000000000..fec42a1f9 Binary files /dev/null and b/fastlane/screenshots/lv/title.strings differ diff --git a/fastlane/screenshots/zh-Hans/title.strings b/fastlane/screenshots/zh-Hans/title.strings new file mode 100644 index 000000000..d9d2b6e1b Binary files /dev/null and b/fastlane/screenshots/zh-Hans/title.strings differ diff --git a/ownCloud File Provider/FileProviderEnumerator.h b/ownCloud File Provider/FileProviderEnumerator.h index adb54f7a8..8e0be0bbb 100644 --- a/ownCloud File Provider/FileProviderEnumerator.h +++ b/ownCloud File Provider/FileProviderEnumerator.h @@ -22,7 +22,7 @@ @class FileProviderExtension; -@interface FileProviderEnumerator : NSObject +@interface FileProviderEnumerator : NSObject { __weak FileProviderExtension *_fileProviderExtension; diff --git a/ownCloud File Provider/FileProviderEnumerator.m b/ownCloud File Provider/FileProviderEnumerator.m index a4a5d9d9c..7bf069997 100644 --- a/ownCloud File Provider/FileProviderEnumerator.m +++ b/ownCloud File Provider/FileProviderEnumerator.m @@ -178,7 +178,7 @@ - (void)_startQuery { // Start query self->_query = [OCQuery queryForPath:queryPath]; - self->_query.includeRootItem = YES; + self->_query.includeRootItem = queryPath.isRootPath; // Include the root item only for the root folder. If it's not included, no folder can be created in the root directory. If a non-root folder is included in a query result for its content, the Files Duplicate action will loop infinitely. self->_query.delegate = self; [DisplaySettings.sharedDisplaySettings updateQueryWithDisplaySettings:self->_query]; @@ -322,9 +322,11 @@ - (void)provideItemsForChangeObserverFromQuery:(OCQuery *)query - (void)queryHasChangesAvailable:(OCQuery *)query { - OCLogDebug(@"##### Query for %@ has changes. Query state: %lu", query.queryPath, (unsigned long)query.state); + OCLogDebug(@"##### Query for %@ has changes. Query state: %lu, SinceSyncAnchor: %@, Changes available: %d", query.queryPath, (unsigned long)query.state, query.querySinceSyncAnchor, query.hasChangesAvailable); - if ((query.state == OCQueryStateContentsFromCache) || ((query.state == OCQueryStateWaitingForServerReply) && (query.queryResults.count > 0)) || (query.state == OCQueryStateIdle)) + if ( (query.state == OCQueryStateContentsFromCache) || + ((query.state == OCQueryStateWaitingForServerReply) && (query.queryResults.count > 0)) || + (query.state == OCQueryStateIdle)) { dispatch_async(dispatch_get_main_queue(), ^{ @synchronized(self) @@ -422,4 +424,14 @@ - (void)currentSyncAnchorWithCompletionHandler:(void (^)(NSFileProviderSyncAncho // - (void)finishEnumeratingWithError:(NSError *)error; // } ++ (NSArray *)logTags +{ + return (@[ @"FPEnum" ]); +} + +- (NSArray *)logTags +{ + return (@[ @"FPEnum", OCLogTagInstance(self)]); +} + @end diff --git a/ownCloud File Provider/FileProviderExtension.m b/ownCloud File Provider/FileProviderExtension.m index 94ff6b72f..3bf6ba552 100644 --- a/ownCloud File Provider/FileProviderExtension.m +++ b/ownCloud File Provider/FileProviderExtension.m @@ -26,6 +26,9 @@ #import "NSError+MessageResolution.h" @interface FileProviderExtension () +{ + NSFileCoordinator *_fileCoordinator; +} @property (nonatomic, readonly, strong) NSFileManager *fileManager; @@ -40,6 +43,8 @@ - (instancetype)init { NSDictionary *bundleInfoDict = [[NSBundle bundleForClass:[FileProviderExtension class]] infoDictionary]; + _fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; + OCCoreManager.sharedCoreManager.memoryConfiguration = OCCoreMemoryConfigurationMinimum; OCAppIdentity.sharedAppIdentity.appIdentifierPrefix = bundleInfoDict[@"OCAppIdentifierPrefix"]; @@ -59,10 +64,15 @@ - (instancetype)init - (void)dealloc { + OCLogDebug(@"Deallocating FileProvider %@", self); + + [_fileCoordinator cancel]; + [self removeObserver:self forKeyPath:@"domain" context:(__bridge void *)self]; if (_core != nil) { + OCLogDebug(@"Returning OCCore for FileProvider %@", self); [[OCCoreManager sharedCoreManager] returnCoreForBookmark:self.bookmark completionHandler:nil]; } } @@ -130,13 +140,13 @@ - (NSFileProviderItem)itemForIdentifier:(NSFileProviderItemIdentifier)identifier } }); - // OCLogDebug(@"-itemForIdentifier:error: %@ => %@", identifier, item); - if ((item == nil) && (returnError == nil)) { returnError = [NSError fileProviderErrorForNonExistentItemWithIdentifier:identifier]; } + // OCLogDebug(@"-itemForIdentifier:error: %@ => %@ / %@", identifier, item, returnError); + if (outError != NULL) { *outError = [returnError translatedError]; @@ -386,8 +396,14 @@ - (void)createDirectoryWithName:(NSString *)directoryName inParentItemIdentifier if ((existingItem = [self.core cachedItemInParent:parentItem withName:directoryName isDirectory:YES error:NULL]) != nil) { FPLogCmd(@"Completed with collission with existingItem=%@ (locally detected)", existingItem); - // completionHandler(nil, [NSError fileProviderErrorForCollisionWithItem:existingItem]); // This is what we should do according to docs - completionHandler(nil, [OCError(OCErrorItemAlreadyExists) translatedError]); // This is what we need to do to avoid users running into issues using the broken Files "Duplicate" action + if (@available(iOS 13.3, *)) + { + completionHandler(nil, [NSError fileProviderErrorForCollisionWithItem:existingItem]); // This is what we should do according to docs + } + else + { + completionHandler(nil, [OCError(OCErrorItemAlreadyExists) translatedError]); // This is what we need to do to avoid users running into issues using the broken Files "Duplicate" action + } return; } @@ -513,80 +529,105 @@ - (void)deleteItemWithIdentifier:(NSFileProviderItemIdentifier)itemIdentifier co - (void)importDocumentAtURL:(NSURL *)fileURL toParentItemIdentifier:(NSFileProviderItemIdentifier)parentItemIdentifier completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler { NSError *error = nil; - BOOL isImportingFromVault = NO; - BOOL importByCopying = NO; - NSString *importFileName = fileURL.lastPathComponent; - OCItem *parentItem; + BOOL stopAccess = NO; - FPLogCmdBegin(@"Import", @"Start of importDocumentAtURL=%@, toParentItemIdentifier=%@", fileURL, parentItemIdentifier); + if ([fileURL startAccessingSecurityScopedResource]) + { + stopAccess = YES; + } - // Detect import of documents from our own internal storage (=> used by Files.app for duplication of files) - isImportingFromVault = [fileURL.path hasPrefix:self.core.vault.filesRootURL.path]; + FPLogCmdBegin(@"Import", @"Start of importDocumentAtURL=%@, toParentItemIdentifier=%@, attributes=%@", fileURL, parentItemIdentifier, [NSFileManager.defaultManager attributesOfItemAtPath:fileURL.path error:nil]); - if (isImportingFromVault) - { - NSFileProviderItemIdentifier sourceItemIdentifier; - NSFileProviderItem sourceItem; + [_fileCoordinator coordinateReadingItemAtURL:fileURL options:NSFileCoordinatorReadingWithoutChanges|NSFileCoordinatorReadingForUploading error:&error byAccessor:^(NSURL * _Nonnull readURL) { + NSError *error = nil; + BOOL isImportingFromVault = NO; + BOOL importByCopying = NO; + NSString *importFileName = readURL.lastPathComponent; + OCItem *parentItem; - // Determine source item - if (((sourceItemIdentifier = [self persistentIdentifierForItemAtURL:fileURL]) != nil) && - ((sourceItem = [self itemForIdentifier:sourceItemIdentifier error:nil]) != nil)) - { - importByCopying = YES; - } - } + FPLogCmd(@"Coordinated read of readURL=%@, toParentItemIdentifier=%@, attributes=%@", readURL, parentItemIdentifier, [NSFileManager.defaultManager attributesOfItemAtPath:readURL.path error:nil]); - if ((parentItem = (OCItem *)[self itemForIdentifier:parentItemIdentifier error:&error]) != nil) - { - // Detect name collissions - OCItem *existingItem; + // Detect import of documents from our own internal storage (=> used by Files.app for duplication of files) + isImportingFromVault = [readURL.path hasPrefix:self.core.vault.filesRootURL.path]; - if ((existingItem = [self.core cachedItemInParent:parentItem withName:importFileName isDirectory:NO error:NULL]) != nil) + if (isImportingFromVault) { - // Return collission error - FPLogCmd(@"Completed with collission with existingItem=%@ (local)", existingItem); - completionHandler(nil, [NSError fileProviderErrorForCollisionWithItem:existingItem]); - return; + NSFileProviderItemIdentifier sourceItemIdentifier; + NSFileProviderItem sourceItem; + + // Determine source item + if (((sourceItemIdentifier = [self persistentIdentifierForItemAtURL:readURL]) != nil) && + ((sourceItem = [self itemForIdentifier:sourceItemIdentifier error:nil]) != nil)) + { + importByCopying = YES; + } } - FPLogCmd(@"Importing %@ at %@ fromURL %@", importFileName, parentItem, fileURL); + if ((parentItem = (OCItem *)[self itemForIdentifier:parentItemIdentifier error:&error]) != nil) + { + // Detect name collissions + OCItem *existingItem; - // Import item - [self.core importItemNamed:importFileName at:parentItem fromURL:fileURL isSecurityScoped:YES options:@{ - OCCoreOptionImportByCopying : @(importByCopying) - } placeholderCompletionHandler:^(NSError *error, OCItem *item) { - FPLogCmd(@"Completed with placeholderItem=%@, error=%@", item, error); - completionHandler(item, [error translatedError]); - } resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { - if ([error.domain isEqual:OCHTTPStatusErrorDomain] && (error.code == OCHTTPStatusCodePRECONDITION_FAILED)) + if ((existingItem = [self.core cachedItemInParent:parentItem withName:importFileName isDirectory:NO error:NULL]) != nil) { - // Collission: file already exists - if ((parameter != nil) && ([parameter isKindOfClass:[OCItem class]])) + // Return collission error + FPLogCmd(@"Completed with collission with existingItem=%@ (local)", existingItem); + completionHandler(nil, [NSError fileProviderErrorForCollisionWithItem:existingItem]); + return; + } + + FPLogCmd(@"Importing %@ at %@ readURL %@", importFileName, parentItem, readURL); + + // Import item + [self.core importItemNamed:importFileName at:parentItem fromURL:readURL isSecurityScoped:YES options:@{ + OCCoreOptionImportByCopying : @(importByCopying) + } placeholderCompletionHandler:^(NSError *error, OCItem *item) { + FPLogCmd(@"Completed with placeholderItem=%@, error=%@", item, error); + completionHandler(item, [error translatedError]); + } resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { + if ([error.domain isEqual:OCHTTPStatusErrorDomain] && (error.code == OCHTTPStatusCodePRECONDITION_FAILED)) { - OCItem *placeholderItem = (OCItem *)parameter; + // Collission: file already exists + if ((parameter != nil) && ([parameter isKindOfClass:[OCItem class]])) + { + OCItem *placeholderItem = (OCItem *)parameter; - // TODO (defunct): - // Upload errors (such as NSFileProviderErrorInsufficientQuota) should be handled - // with a subsequent update to the [placeholder] item, setting its uploadingError property. + // TODO (defunct): + // Upload errors (such as NSFileProviderErrorInsufficientQuota) should be handled + // with a subsequent update to the [placeholder] item, setting its uploadingError property. - // TODO (not yet implemented): - // Upload errors should not prevent creating or importing a document, because they - // can be resolved at a later date (for example, when the user has quota again.) + // TODO (not yet implemented): + // Upload errors should not prevent creating or importing a document, because they + // can be resolved at a later date (for example, when the user has quota again.) - if (placeholderItem.isPlaceholder) - { - FPLogCmd(@"Completed with fileAlreadyExistsAs=%@", placeholderItem); - [placeholderItem setUploadingError:[NSError fileProviderErrorForCollisionWithItem:placeholderItem]]; + if (placeholderItem.isPlaceholder) + { + FPLogCmd(@"Completed with fileAlreadyExistsAs=%@", placeholderItem); + [placeholderItem setUploadingError:[NSError fileProviderErrorForCollisionWithItem:placeholderItem]]; + } } } - } - }]; - } - else + }]; + } + else + { + FPLogCmd(@"Completed with parentItem=%@ not found, error=%@", parentItem, error); + completionHandler(nil, error); + } + + if (stopAccess) + { + [readURL stopAccessingSecurityScopedResource]; + } + }]; + + FPLogCmd(@"File Coordinator returned with error=%@", error); + + if (error != nil) { - FPLogCmd(@"Completed with parentItem=%@ not found, error=%@", parentItem, error); completionHandler(nil, error); } + } - (void)setFavoriteRank:(NSNumber *)favoriteRank forItemIdentifier:(NSFileProviderItemIdentifier)itemIdentifier completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler @@ -851,6 +892,8 @@ - (OCCore *)core { if (self.bookmark != nil) { + OCLogDebug(@"Requesting OCCore for FileProvider %@", self); + OCSyncExec(waitForCore, { [[OCCoreManager sharedCoreManager] requestCoreForBookmark:self.bookmark setup:^(OCCore *core, NSError *error) { self->_core = core; diff --git a/ownCloud File Provider/Info.plist b/ownCloud File Provider/Info.plist index cdda3cc10..7b04cd34b 100644 --- a/ownCloud File Provider/Info.plist +++ b/ownCloud File Provider/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 144 + 149 NSExtension NSExtensionFileProviderDocumentGroup diff --git a/ownCloud File ProviderUI/Info.plist b/ownCloud File ProviderUI/Info.plist index e090e8e0f..58850a279 100644 --- a/ownCloud File ProviderUI/Info.plist +++ b/ownCloud File ProviderUI/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 144 + 149 NSExtension NSExtensionFileProviderActions diff --git a/ownCloud Intents/Info.plist b/ownCloud Intents/Info.plist new file mode 100644 index 000000000..3bbe97d22 --- /dev/null +++ b/ownCloud Intents/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + SiriKit + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(APP_SHORT_VERSION) + CFBundleVersion + 140 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsRestrictedWhileProtectedDataUnavailable + + IntentsSupported + + CreateFolderIntent + DeletePathItemIntent + GetAccountIntent + GetAccountsIntent + GetDirectoryListingIntent + GetFileInfoIntent + GetFileIntent + PathExistsIntent + SaveFileIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + OCAppGroupIdentifier + group.com.owncloud.ios-app + OCAppIdentifierPrefix + $(AppIdentifierPrefix) + OCHasFileProvider + + OCKeychainAccessGroupIdentifier + group.com.owncloud.ios-app + + diff --git a/ownCloud Intents/IntentHandler.swift b/ownCloud Intents/IntentHandler.swift new file mode 100644 index 000000000..384eed5d4 --- /dev/null +++ b/ownCloud Intents/IntentHandler.swift @@ -0,0 +1,47 @@ +// +// IntentHandler.swift +// SiriKit +// +// Created by Matthias Hühne on 23.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import Intents +import ownCloudAppShared + +class IntentHandler: INExtension { + + override func handler(for intent: INIntent) -> Any { + if intent is GetAccountsIntent { + return GetAccountsIntentHandler() + } else if intent is GetAccountIntent { + return GetAccountIntentHandler() + } else if intent is GetDirectoryListingIntent { + return GetDirectoryListingIntentHandler() + } else if intent is GetFileIntent { + return GetFileIntentHandler() + } else if intent is SaveFileIntent { + return SaveFileIntentHandler() + } else if intent is CreateFolderIntent { + return CreateFolderIntentHandler() + } else if intent is GetFileInfoIntent { + return GetFileInfoIntentHandler() + } else if intent is PathExistsIntent { + return PathExistsIntentHandler() + } else if intent is DeletePathItemIntent { + return DeletePathItemIntentHandler() + } + + fatalError("Unhandled intent type: \(intent)") + } +} diff --git a/ownCloud Intents/ownCloud Intents.entitlements b/ownCloud Intents/ownCloud Intents.entitlements new file mode 100644 index 000000000..988c47786 --- /dev/null +++ b/ownCloud Intents/ownCloud Intents.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.com.owncloud.ios-app + + keychain-access-groups + + $(AppIdentifierPrefix)group.com.owncloud.ios-app + + + diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 6dc4316a3..c6bcf1f41 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 233BDEAA204FEFE500C06732 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 233BDEA8204FEFE500C06732 /* LaunchScreen.storyboard */; }; 233BDEB5204FEFE500C06732 /* OwnCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233BDEB4204FEFE500C06732 /* OwnCloudTests.swift */; }; 233E0FD82099F11D00C3D8D5 /* SecuritySettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E0FD72099F11D00C3D8D5 /* SecuritySettingsSection.swift */; }; - 2347446A20761BB700859C93 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2347446920761BB700859C93 /* String+Extension.swift */; }; 236735A621217C3500E5834A /* MoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236735A521217C3500E5834A /* MoreViewController.swift */; }; 23957A6D209AFFE8003C8537 /* MoreSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23957A6C209AFFE8003C8537 /* MoreSettingsSection.swift */; }; 239F1319205A693A0029F186 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239F1318205A693A0029F186 /* UIColor+Extension.swift */; }; @@ -30,17 +29,34 @@ 23EC77592137F3DD0032D4E6 /* DisplayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC77552137F3DC0032D4E6 /* DisplayExtension.swift */; }; 23EC775B2137F3DD0032D4E6 /* OCExtensionType+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC77572137F3DD0032D4E6 /* OCExtensionType+Extension.swift */; }; 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */; }; - 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F6238020B587EF004FDE8B /* SortMethod.swift */; }; 23FA23E620BFD3D8009A6D73 /* SortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FA23E520BFD3D8009A6D73 /* SortBar.swift */; }; + 3900348223A100D3000D8510 /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3900348123A100D3000D8510 /* UIApplication+Extension.swift */; }; + 39057AA3233BA7A60008E6C0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; + 39057AA4233BA7A60008E6C0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; + 39057AA7233BA7A60008E6C0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; + 39057AA8233BA7A60008E6C0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; 390B51E02292DBB100935E24 /* SharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */; }; 39104E10223991C8002FC02F /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39104E0A223991C8002FC02F /* UIButton+Extension.swift */; }; + 3912208223436EB80026C290 /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3912208123436EB80026C290 /* SortMethod.swift */; }; 3913213822946E5E00EF88F4 /* FileListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */; }; 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */; }; + 392557FE2278703300E83F60 /* UISearchBar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392557FD2278703300E83F60 /* UISearchBar+Extension.swift */; }; + 3940C4F02326985B008227AE /* GetAccountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3940C4EF2326985B008227AE /* GetAccountIntentHandler.swift */; }; 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */; }; + 394A0AED22EEE32C00603813 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A7138222E79C6700089423 /* IntentHandler.swift */; }; 394E1FDA233E2D64009D2897 /* FavoriteAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FD9233E2D64009D2897 /* FavoriteAction.swift */; }; + 394A0AFD22EEFC2C00603813 /* ownCloudAppShared.h in Headers */ = {isa = PBXBuildFile; fileRef = 394A0AFB22EEFC2C00603813 /* ownCloudAppShared.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 394A0B0022EEFC2C00603813 /* ownCloudAppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; }; + 394A0B0122EEFC2C00603813 /* ownCloudAppShared.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 394A0B0522EEFCA300603813 /* GetAccountsIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A7139722E858E400089423 /* GetAccountsIntentHandler.swift */; }; 394E1FDC233E3750009D2897 /* UnfavoriteAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FDB233E3750009D2897 /* UnfavoriteAction.swift */; }; + 394A0B0922EEFCE400603813 /* ownCloudAppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; }; + 394A0B0A22EEFCF500603813 /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; 394E1FFF233E43F5009D2897 /* LinksAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FFE233E43F5009D2897 /* LinksAction.swift */; }; 394E200C233E477F009D2897 /* OpenItemUserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */; }; + 395E16FA22F03CAF00DE89A1 /* GetFileIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395E16F922F03CAF00DE89A1 /* GetFileIntentHandler.swift */; }; + 395E16FD22F06A7300DE89A1 /* SaveFileIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395E16FC22F06A7300DE89A1 /* SaveFileIntentHandler.swift */; }; + 395E16FF22F172C900DE89A1 /* CreateFolderIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395E16FE22F172C900DE89A1 /* CreateFolderIntentHandler.swift */; }; 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */; }; 3961281622F8730A0087BD3A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3961281522F8730A0087BD3A /* SceneDelegate.swift */; }; 3968C881239C54AC00AC28AC /* ReleaseNotesHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3968C879239C54AC00AC28AC /* ReleaseNotesHostViewController.swift */; }; @@ -52,18 +68,27 @@ 396D7C6523224A53002380C1 /* DiscardSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */; }; 3971B48F221B23FE006FB441 /* ThemeableColoredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */; }; 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754F22327A33500119FCB /* OpenSceneAction.swift */; }; + 397754E223279EED00119FCB /* OCItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754E123279EED00119FCB /* OCItem+Extension.swift */; }; + 3984F5712319202200DC2639 /* DeletePathItemIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984F56B2319202200DC2639 /* DeletePathItemIntentHandler.swift */; }; 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */; }; + 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */; }; 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */; }; 3998F5D3224102FE00B66713 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */; }; 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D422411EDF00B66713 /* BorderedLabel.swift */; }; 3998F5D72241486F00B66713 /* OCCertificate+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */; }; + 399A4C002317CC460027DDD6 /* AppLockHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4BFF2317CC460027DDD6 /* AppLockHelper.swift */; }; + 399A4C032317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */; }; + 399A4C1023190ADF0027DDD6 /* PathExistsIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */; }; 399DD7C722A691BC00B45EB2 /* UnshareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399DD7C122A691BC00B45EB2 /* UnshareAction.swift */; }; 39A5135322608836002CF1AA /* OCShare+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A5135222608836002CF1AA /* OCShare+Extension.swift */; }; 39A513AC22674E56002CF1AA /* OCCore+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A513AB22674E56002CF1AA /* OCCore+Extension.swift */; }; + 39A5C3A1231566D9009D9EE3 /* GetFileInfoIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A5C3A0231566D9009D9EE3 /* GetFileInfoIntentHandler.swift */; }; + 39A7138722E79C6700089423 /* ownCloud Intents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 39A7138022E79C6700089423 /* ownCloud Intents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 39AFC3D1225E72FB00A6D3AE /* GroupSharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AFC3D0225E72FB00A6D3AE /* GroupSharingTableViewController.swift */; }; 39AFC3D8225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AFC3D7225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift */; }; 39B289A8226F1EE000BE0E11 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B289A7226F1EE000BE0E11 /* MessageView.swift */; }; 39B9675022BE0FBA0074DB22 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 593A821320C7D4C5000E2A90 /* Localizable.strings */; }; + 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BE385C23435AFE0062A2FE /* String+Extension.swift */; }; 39CC8AE6228C12100020253B /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8AE5228C12100020253B /* Array+Extension.swift */; }; 39CC8B01228C8A950020253B /* MediaUploadSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */; }; 39CC8B37228D5B890020253B /* ShareClientItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */; }; @@ -73,14 +98,20 @@ 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */; }; 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */; }; 39E42D1C2315288B00B82AC3 /* KeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */; }; + 39E6DE84233CC39A008DAE04 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; }; + 39E6DE86233CDF1E008DAE04 /* OCItemTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */; }; 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */; }; 39E98B452279ACF5009911F1 /* PublicLinkEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */; }; + 39F689AB22F018C100E63429 /* GetDirectoryListingIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F689AA22F018C100E63429 /* GetDirectoryListingIntentHandler.swift */; }; + 39F689AC22F0206900E63429 /* OCBookmark+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */; }; 46B9D336BF7FE50321823888 /* Pods_ownCloudScreenshotsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54199937F74A129BC74DEB0A /* Pods_ownCloudScreenshotsTests.framework */; }; + 4C05D8A5238708D40073EF50 /* MediaUploadStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C05D8A4238708D40073EF50 /* MediaUploadStorage.swift */; }; 4C11EE5B22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */; }; 4C1561E8222321E0009C4EF3 /* PhotoSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */; }; 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561EE22232357009C4EF3 /* PhotoSelectionViewCell.swift */; }; 4C16CBA7226F0F1A00D67BB6 /* FileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C16CBA6226F0F1900D67BB6 /* FileTests.swift */; }; 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */; }; + 4C3E17DB234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3E17DA234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift */; }; 4C464BEF2187AF1500D30602 /* PDFThumbnailCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE12187AF1400D30602 /* PDFThumbnailCollectionViewCell.swift */; }; 4C464BF02187AF1500D30602 /* PDFTocTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE82187AF1400D30602 /* PDFTocTableViewController.swift */; }; 4C464BF12187AF1500D30602 /* PDFTocTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE92187AF1400D30602 /* PDFTocTableViewCell.swift */; }; @@ -98,6 +129,7 @@ 4C7295D8228C384E00FA4E68 /* LogFilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7295D7228C384E00FA4E68 /* LogFilesViewController.swift */; }; 4C82D07022C9387300835F0B /* MediaDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82D06F22C9387300835F0B /* MediaDisplayViewController.swift */; }; 4C88041822E78D790016CBA9 /* MediaFilesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88041722E78D790016CBA9 /* MediaFilesSettings.swift */; }; + 4C96463C238489E4003278B7 /* MediaUploadActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C96463B238489E4003278B7 /* MediaUploadActivity.swift */; }; 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BFA2223158C3F0059CA3E /* PreviewViewController.swift */; }; 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAF783B2282FD40000C85CF /* FileManager+Extension.swift */; }; 4CB8ADDE22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */; }; @@ -363,6 +395,34 @@ remoteGlobalIDString = DC7E0A77203732B3006111FA; remoteInfo = Ocean; }; + 394A0AFE22EEFC2C00603813 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDE94204FEFE500C06732 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 394A0AF822EEFC2C00603813; + remoteInfo = ownCloudAppShared; + }; + 394A0B0722EEFCD900603813 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDE94204FEFE500C06732 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 394A0AF822EEFC2C00603813; + remoteInfo = ownCloudAppShared; + }; + 394A0B0B22EEFD2800603813 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDEBF204FEFF300C06732 /* ownCloudSDK.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = DCC8F9AA202852A200EB6701; + remoteInfo = ownCloudSDK; + }; + 39A7138522E79C6700089423 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDE94204FEFE500C06732 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 39A7137F22E79C6700089423; + remoteInfo = SiriKit; + }; 59056CAF22414F3C00A18A22 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 233BDE94204FEFE500C06732 /* Project object */; @@ -534,6 +594,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 39A7138D22E79DDE00089423 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 59799F7C224157E0007E8008 /* EarlGrey Copy Files */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -574,6 +644,7 @@ dstSubfolderSpec = 10; files = ( DC63207E21FCA731007EC0A8 /* libzip.framework in Copy Frameworks */, + 394A0B0122EEFC2C00603813 /* ownCloudAppShared.framework in Copy Frameworks */, DCC085722293F1FD008CC05C /* ownCloudApp.framework in Copy Frameworks */, DCFBAD0C21BE67A100943F76 /* ownCloudUI.framework in Copy Frameworks */, DC7DBA20207F5A0400E7337D /* PocketSVG.framework in Copy Frameworks */, @@ -589,6 +660,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + 39A7138722E79C6700089423 /* ownCloud Intents.appex in Embed App Extensions */, DCC6566520C9B7E400110A97 /* ownCloud File Provider.appex in Embed App Extensions */, ); name = "Embed App Extensions"; @@ -612,7 +684,6 @@ 233BDEB6204FEFE500C06732 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 233BDEBF204FEFF300C06732 /* ownCloudSDK.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ownCloudSDK.xcodeproj; path = "ios-sdk/ownCloudSDK.xcodeproj"; sourceTree = ""; }; 233E0FD72099F11D00C3D8D5 /* SecuritySettingsSection.swift */ = {isa = PBXFileReference; indentWidth = 8; lastKnownFileType = sourcecode.swift; path = SecuritySettingsSection.swift; sourceTree = ""; tabWidth = 8; usesTabs = 1; }; - 2347446920761BB700859C93 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 236735A521217C3500E5834A /* MoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MoreViewController.swift; sourceTree = ""; }; 23957A6C209AFFE8003C8537 /* MoreSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreSettingsSection.swift; sourceTree = ""; }; 239F1318205A693A0029F186 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; @@ -627,17 +698,28 @@ 23EC77552137F3DC0032D4E6 /* DisplayExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayExtension.swift; sourceTree = ""; }; 23EC77572137F3DD0032D4E6 /* OCExtensionType+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCExtensionType+Extension.swift"; sourceTree = ""; }; 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDisplayViewController.swift; sourceTree = ""; }; - 23F6238020B587EF004FDE8B /* SortMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 23FA23E520BFD3D8009A6D73 /* SortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBar.swift; sourceTree = ""; }; + 3900348123A100D3000D8510 /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; + 39057AA9233BA7A60008E6C0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; + 39057AB1233BA7AE0008E6C0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intents.strings; sourceTree = ""; }; 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingTableViewController.swift; sourceTree = ""; }; 39104E0A223991C8002FC02F /* UIButton+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; + 3912208123436EB80026C290 /* SortMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListTableViewController.swift; sourceTree = ""; }; 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryTableViewController.swift; sourceTree = ""; }; + 392557FD2278703300E83F60 /* UISearchBar+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Extension.swift"; sourceTree = ""; }; + 3940C4EF2326985B008227AE /* GetAccountIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetAccountIntentHandler.swift; sourceTree = ""; }; 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadCrumbTableViewController.swift; sourceTree = ""; }; + 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ownCloudAppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 394E1FD9233E2D64009D2897 /* FavoriteAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteAction.swift; sourceTree = ""; }; + 394A0AFB22EEFC2C00603813 /* ownCloudAppShared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ownCloudAppShared.h; sourceTree = ""; }; + 394A0AFC22EEFC2C00603813 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 394E1FDB233E3750009D2897 /* UnfavoriteAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnfavoriteAction.swift; sourceTree = ""; }; 394E1FFE233E43F5009D2897 /* LinksAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinksAction.swift; sourceTree = ""; }; 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenItemUserActivity.swift; sourceTree = ""; }; + 395E16F922F03CAF00DE89A1 /* GetFileIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFileIntentHandler.swift; sourceTree = ""; }; + 395E16FC22F06A7300DE89A1 /* SaveFileIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveFileIntentHandler.swift; sourceTree = ""; }; + 395E16FE22F172C900DE89A1 /* CreateFolderIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFolderIntentHandler.swift; sourceTree = ""; }; 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Extension.swift"; sourceTree = ""; }; 3961281522F8730A0087BD3A /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 3968C879239C54AC00AC28AC /* ReleaseNotesHostViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReleaseNotesHostViewController.swift; sourceTree = ""; }; @@ -648,20 +730,34 @@ 396C82FA2319AFDD00938262 /* CollaborateAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollaborateAction.swift; sourceTree = ""; }; 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardSceneAction.swift; sourceTree = ""; }; 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeableColoredView.swift; sourceTree = ""; }; + 397754E123279EED00119FCB /* OCItem+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCItem+Extension.swift"; sourceTree = ""; }; 397754F22327A33500119FCB /* OpenSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSceneAction.swift; sourceTree = ""; }; + 3984F56B2319202200DC2639 /* DeletePathItemIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeletePathItemIntentHandler.swift; sourceTree = ""; }; 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; - 39880BAA233B5236006EA539 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; + 39880BAA233B5236006EA539 /* eu */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; 39880BB0233B524B006EA539 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; + 39954DDF23A39549006B6DC0 /* ar */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extension.swift"; sourceTree = ""; }; + 39954DE023A39625006B6DC0 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedInfoView.swift; sourceTree = ""; }; 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extension.swift"; sourceTree = ""; }; 3998F5D422411EDF00B66713 /* BorderedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedLabel.swift; sourceTree = ""; }; 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCCertificate+Extension.swift"; sourceTree = ""; }; + 399A4BFF2317CC460027DDD6 /* AppLockHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockHelper.swift; sourceTree = ""; }; + 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmarkManager+Extension.swift"; sourceTree = ""; }; + 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathExistsIntentHandler.swift; sourceTree = ""; }; 399DD7C122A691BC00B45EB2 /* UnshareAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnshareAction.swift; sourceTree = ""; }; 39A5135222608836002CF1AA /* OCShare+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCShare+Extension.swift"; sourceTree = ""; }; 39A513AB22674E56002CF1AA /* OCCore+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCCore+Extension.swift"; sourceTree = ""; }; + 39A5C3A0231566D9009D9EE3 /* GetFileInfoIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFileInfoIntentHandler.swift; sourceTree = ""; }; + 39A7138022E79C6700089423 /* ownCloud Intents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "ownCloud Intents.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 39A7138222E79C6700089423 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + 39A7138422E79C6700089423 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 39A7139722E858E400089423 /* GetAccountsIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAccountsIntentHandler.swift; sourceTree = ""; }; 39AFC3D0225E72FB00A6D3AE /* GroupSharingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSharingTableViewController.swift; sourceTree = ""; }; 39AFC3D7225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSharingEditTableViewController.swift; sourceTree = ""; }; 39B289A7226F1EE000BE0E11 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; + 39BE385C23435AFE0062A2FE /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 39CC8AE5228C12100020253B /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUploadSettingsSection.swift; sourceTree = ""; }; 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientItemCell.swift; sourceTree = ""; }; @@ -671,15 +767,20 @@ 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableHeaderView.swift; sourceTree = ""; }; 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeRoundedButton.swift; sourceTree = ""; }; 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCommands.swift; sourceTree = ""; }; + 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCItemTracker.swift; sourceTree = ""; }; 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkTableViewController.swift; sourceTree = ""; }; 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkEditTableViewController.swift; sourceTree = ""; }; + 39F689A922EF5EDC00E63429 /* ownCloud Intents.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ownCloud Intents.entitlements"; sourceTree = ""; }; + 39F689AA22F018C100E63429 /* GetDirectoryListingIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetDirectoryListingIntentHandler.swift; sourceTree = ""; }; 3D753147564B1E4F47826109 /* Pods-ownCloud Screenshots Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloud Screenshots Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloud Screenshots Tests/Pods-ownCloud Screenshots Tests.debug.xcconfig"; sourceTree = ""; }; 42866B2892DC9EDC65D844E7 /* Pods_ownCloud_Screenshots_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ownCloud_Screenshots_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C05D8A4238708D40073EF50 /* MediaUploadStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadStorage.swift; sourceTree = ""; }; 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantMediaUploadTaskExtension.swift; sourceTree = ""; }; 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSelectionViewController.swift; sourceTree = ""; }; 4C1561EE22232357009C4EF3 /* PhotoSelectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSelectionViewCell.swift; sourceTree = ""; }; 4C16CBA6226F0F1900D67BB6 /* FileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTests.swift; sourceTree = ""; }; 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; + 4C3E17DA234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingMediaUploadTaskExtension.swift; sourceTree = ""; }; 4C464BE12187AF1400D30602 /* PDFThumbnailCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFThumbnailCollectionViewCell.swift; sourceTree = ""; }; 4C464BE82187AF1400D30602 /* PDFTocTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTocTableViewController.swift; sourceTree = ""; }; 4C464BE92187AF1400D30602 /* PDFTocTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTocTableViewCell.swift; sourceTree = ""; }; @@ -697,6 +798,7 @@ 4C7295D7228C384E00FA4E68 /* LogFilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFilesViewController.swift; sourceTree = ""; }; 4C82D06F22C9387300835F0B /* MediaDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDisplayViewController.swift; sourceTree = ""; }; 4C88041722E78D790016CBA9 /* MediaFilesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFilesSettings.swift; sourceTree = ""; }; + 4C96463B238489E4003278B7 /* MediaUploadActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadActivity.swift; sourceTree = ""; }; 4C9BFA2223158C3F0059CA3E /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; 4CAF783B2282FD40000C85CF /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = ""; }; 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPhotoLibrary+Extension.swift"; sourceTree = ""; }; @@ -956,6 +1058,7 @@ DC3BE0D72077BC5D002A0AC0 /* openssl.framework in Frameworks */, DC3BE0D82077BC5D002A0AC0 /* ownCloudSDK.framework in Frameworks */, DCC085712293F1FD008CC05C /* ownCloudApp.framework in Frameworks */, + 394A0B0022EEFC2C00603813 /* ownCloudAppShared.framework in Frameworks */, DCE0275E21F1DF7E00F2544E /* ownCloudUI.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -972,6 +1075,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 394A0AF622EEFC2C00603813 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 394A0B0A22EEFCF500603813 /* ownCloudSDK.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A7137D22E79C6700089423 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 394A0B0922EEFCE400603813 /* ownCloudAppShared.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 59056CA722414F3C00A18A22 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1053,6 +1172,8 @@ DC7DBA3B207F86E900E7337D /* Tools */, DCC6564720C9B7E400110A97 /* ownCloud File Provider */, DCC6565820C9B7E400110A97 /* ownCloud File ProviderUI */, + 39A7138122E79C6700089423 /* ownCloud Intents */, + 394A0AFA22EEFC2C00603813 /* ownCloudAppShared */, 59056CAB22414F3C00A18A22 /* ownCloudScreenshotsTests */, 233BDE9D204FEFE500C06732 /* Products */, DC85573220513CC700189B9A /* Frameworks */, @@ -1071,6 +1192,8 @@ DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */, DCC085642293F1FD008CC05C /* ownCloudAppTests.xctest */, 59056CAA22414F3C00A18A22 /* ownCloudScreenshotsTests.xctest */, + 39A7138022E79C6700089423 /* ownCloud Intents.appex */, + 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */, ); name = Products; sourceTree = ""; @@ -1172,7 +1295,6 @@ children = ( 39104E0A223991C8002FC02F /* UIButton+Extension.swift */, 239F1318205A693A0029F186 /* UIColor+Extension.swift */, - 2347446920761BB700859C93 /* String+Extension.swift */, DCE974BB207EACA60069FC2B /* UIImage+Extension.swift */, DC018F8220A0F56300135198 /* UIView+Extension.swift */, 6E83C78320A33C180066EC23 /* LAContext+Extension.swift */, @@ -1186,6 +1308,7 @@ 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */, 39CC8AE5228C12100020253B /* Array+Extension.swift */, 4C63F0E4231A91230088E8CA /* UIImageView+Thumbnails.swift */, + 3900348123A100D3000D8510 /* UIApplication+Extension.swift */, ); path = "UIKit Extensions"; sourceTree = ""; @@ -1220,6 +1343,14 @@ path = Sharing; sourceTree = ""; }; + 3912208023436E9B0026C290 /* Client */ = { + isa = PBXGroup; + children = ( + 3912208123436EB80026C290 /* SortMethod.swift */, + ); + path = Client; + sourceTree = ""; + }; 3918FE742287DB9B00BACE03 /* Library */ = { isa = PBXGroup; children = ( @@ -1230,6 +1361,38 @@ path = Library; sourceTree = ""; }; + 394A0AFA22EEFC2C00603813 /* ownCloudAppShared */ = { + isa = PBXGroup; + children = ( + 3912208023436E9B0026C290 /* Client */, + 399725DF233DF37300FC3B94 /* UIKit Extension */, + 397754E023279EC100119FCB /* SDK Extensions */, + 399A4BF92317CC300027DDD6 /* Tools */, + 395E16FB22F0691F00DE89A1 /* Intent */, + 394A0AFB22EEFC2C00603813 /* ownCloudAppShared.h */, + 394A0AFC22EEFC2C00603813 /* Info.plist */, + ); + path = ownCloudAppShared; + sourceTree = ""; + }; + 395E16FB22F0691F00DE89A1 /* Intent */ = { + isa = PBXGroup; + children = ( + 399A4C012317D1BA0027DDD6 /* Extensions */, + 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */, + 3940C4EF2326985B008227AE /* GetAccountIntentHandler.swift */, + 39A7139722E858E400089423 /* GetAccountsIntentHandler.swift */, + 39F689AA22F018C100E63429 /* GetDirectoryListingIntentHandler.swift */, + 395E16F922F03CAF00DE89A1 /* GetFileIntentHandler.swift */, + 395E16FC22F06A7300DE89A1 /* SaveFileIntentHandler.swift */, + 395E16FE22F172C900DE89A1 /* CreateFolderIntentHandler.swift */, + 39A5C3A0231566D9009D9EE3 /* GetFileInfoIntentHandler.swift */, + 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */, + 3984F56B2319202200DC2639 /* DeletePathItemIntentHandler.swift */, + ); + path = Intent; + sourceTree = ""; + }; 394E2005233E4765009D2897 /* Window */ = { isa = PBXGroup; children = ( @@ -1256,6 +1419,58 @@ path = "Key Commands"; sourceTree = ""; }; + 397328E822D6067B006CFAA4 /* Import */ = { + isa = PBXGroup; + children = ( + 397328EE22D606AC006CFAA4 /* ImportFilesController.swift */, + ); + path = Import; + sourceTree = ""; + }; + 397754E023279EC100119FCB /* SDK Extensions */ = { + isa = PBXGroup; + children = ( + 397754E123279EED00119FCB /* OCItem+Extension.swift */, + 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */, + ); + path = "SDK Extensions"; + sourceTree = ""; + }; + 399725DF233DF37300FC3B94 /* UIKit Extension */ = { + isa = PBXGroup; + children = ( + 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */, + 39BE385C23435AFE0062A2FE /* String+Extension.swift */, + ); + path = "UIKit Extension"; + sourceTree = ""; + }; + 399A4BF92317CC300027DDD6 /* Tools */ = { + isa = PBXGroup; + children = ( + 399A4BFF2317CC460027DDD6 /* AppLockHelper.swift */, + ); + path = Tools; + sourceTree = ""; + }; + 399A4C012317D1BA0027DDD6 /* Extensions */ = { + isa = PBXGroup; + children = ( + 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 39A7138122E79C6700089423 /* ownCloud Intents */ = { + isa = PBXGroup; + children = ( + 39F689A922EF5EDC00E63429 /* ownCloud Intents.entitlements */, + 39A7138222E79C6700089423 /* IntentHandler.swift */, + 39A7138422E79C6700089423 /* Info.plist */, + ); + path = "ownCloud Intents"; + sourceTree = ""; + }; 4C51727422DE04BD001BC97F /* Tasks */ = { isa = PBXGroup; children = ( @@ -1263,6 +1478,7 @@ 4C51727622DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift */, 4C51727722DE04BD001BC97F /* ScheduledTaskManager.swift */, 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */, + 4C3E17DA234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift */, ); path = Tasks; sourceTree = ""; @@ -1281,6 +1497,8 @@ 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */, 4CB8ADE222DF6BA700F1FEBC /* PHAsset+Upload.swift */, 4CC4A21822FB4F4C00AE7E2C /* MediaUploadQueue.swift */, + 4C96463B238489E4003278B7 /* MediaUploadActivity.swift */, + 4C05D8A4238708D40073EF50 /* MediaUploadStorage.swift */, ); path = "PhotoKit Extensions"; sourceTree = ""; @@ -1496,7 +1714,6 @@ DC63208221FCAC1E007EC0A8 /* ClientActivityViewController.swift */, DC3BE0DD2077CC13002A0AC0 /* ClientRootViewController.swift */, DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */, - 23F6238020B587EF004FDE8B /* SortMethod.swift */, 23FA23E520BFD3D8009A6D73 /* SortBar.swift */, 4C6B780F2226B83300C5F3DB /* PhotoAlbumTableViewController.swift */, 4C6B78112226B86300C5F3DB /* PhotoAlbumTableViewCell.swift */, @@ -1939,6 +2156,14 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 394A0AF422EEFC2C00603813 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 394A0AFD22EEFC2C00603813 /* ownCloudAppShared.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DCC085572293F1FD008CC05C /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -1980,6 +2205,8 @@ DCC6566120C9B7E400110A97 /* PBXTargetDependency */, DCC6566420C9B7E400110A97 /* PBXTargetDependency */, DCC085702293F1FD008CC05C /* PBXTargetDependency */, + 39A7138622E79C6700089423 /* PBXTargetDependency */, + 394A0AFF22EEFC2C00603813 /* PBXTargetDependency */, ); name = ownCloud; productName = ownCloud; @@ -2010,6 +2237,44 @@ productReference = 233BDEB0204FEFE500C06732 /* ownCloudTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 394A0AF822EEFC2C00603813 /* ownCloudAppShared */ = { + isa = PBXNativeTarget; + buildConfigurationList = 394A0B0222EEFC2C00603813 /* Build configuration list for PBXNativeTarget "ownCloudAppShared" */; + buildPhases = ( + 394A0AF422EEFC2C00603813 /* Headers */, + 394A0AF522EEFC2C00603813 /* Sources */, + 394A0AF622EEFC2C00603813 /* Frameworks */, + 394A0AF722EEFC2C00603813 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 394A0B0C22EEFD2800603813 /* PBXTargetDependency */, + ); + name = ownCloudAppShared; + productName = ownCloudAppShared; + productReference = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; + productType = "com.apple.product-type.framework"; + }; + 39A7137F22E79C6700089423 /* ownCloud Intents */ = { + isa = PBXNativeTarget; + buildConfigurationList = 39A7138822E79C6700089423 /* Build configuration list for PBXNativeTarget "ownCloud Intents" */; + buildPhases = ( + 39A7137C22E79C6700089423 /* Sources */, + 39A7137D22E79C6700089423 /* Frameworks */, + 39A7137E22E79C6700089423 /* Resources */, + 39A7138D22E79DDE00089423 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 394A0B0822EEFCD900603813 /* PBXTargetDependency */, + ); + name = "ownCloud Intents"; + productName = SiriKit; + productReference = 39A7138022E79C6700089423 /* ownCloud Intents.appex */; + productType = "com.apple.product-type.app-extension"; + }; 59056CA922414F3C00A18A22 /* ownCloudScreenshotsTests */ = { isa = PBXNativeTarget; buildConfigurationList = 59056CB122414F3C00A18A22 /* Build configuration list for PBXNativeTarget "ownCloudScreenshotsTests" */; @@ -2128,7 +2393,7 @@ 233BDE94204FEFE500C06732 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1010; + LastSwiftUpdateCheck = 1100; LastUpgradeCheck = 1020; ORGANIZATIONNAME = "ownCloud GmbH"; TargetAttributes = { @@ -2157,6 +2422,14 @@ ProvisioningStyle = Automatic; TestTargetID = 233BDE9B204FEFE500C06732; }; + 394A0AF822EEFC2C00603813 = { + CreatedOnToolsVersion = 11.0; + ProvisioningStyle = Automatic; + }; + 39A7137F22E79C6700089423 = { + CreatedOnToolsVersion = 11.0; + ProvisioningStyle = Automatic; + }; 59056CA922414F3C00A18A22 = { CreatedOnToolsVersion = 10.1; ProvisioningStyle = Automatic; @@ -2168,6 +2441,7 @@ }; DCC0855B2293F1FD008CC05C = { CreatedOnToolsVersion = 10.2.1; + LastSwiftMigration = 1100; ProvisioningStyle = Automatic; }; DCC085632293F1FD008CC05C = { @@ -2213,6 +2487,7 @@ "pt-PT", "th-TH", eu, + ar, ); mainGroup = 233BDE93204FEFE500C06732; productRefGroup = 233BDE9D204FEFE500C06732 /* Products */; @@ -2242,7 +2517,9 @@ DC7DBA33207F84BF00E7337D /* MakeTVG */, DCC6564520C9B7E300110A97 /* ownCloud File Provider */, DCC6565620C9B7E400110A97 /* ownCloud File ProviderUI */, + 39A7137F22E79C6700089423 /* ownCloud Intents */, DCC0855B2293F1FD008CC05C /* ownCloudApp */, + 394A0AF822EEFC2C00603813 /* ownCloudAppShared */, DCC085632293F1FD008CC05C /* ownCloudAppTests */, 59056CA922414F3C00A18A22 /* ownCloudScreenshotsTests */, ); @@ -2387,6 +2664,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 394A0AF722EEFC2C00603813 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A7137E22E79C6700089423 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 59056CA822414F3C00A18A22 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2571,7 +2862,6 @@ files = ( 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */, DC3DEC7B22AFA1F000F3352D /* DownloadItemsHUDViewController.swift in Sources */, - 2347446A20761BB700859C93 /* String+Extension.swift in Sources */, DCF4F17920519F8C00189B9A /* StaticTableViewController.swift in Sources */, DC680576212DF548006C3B1F /* CertificateManagementViewController.swift in Sources */, 4CB8ADE322DF6BA700F1FEBC /* PHAsset+Upload.swift in Sources */, @@ -2621,6 +2911,7 @@ 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */, DC1B2709209CF0D3004715E1 /* CertificateViewController.swift in Sources */, DC248C67213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift in Sources */, + 4C3E17DB234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift in Sources */, 6E586CFC2199A72600F680C4 /* OpenInAction.swift in Sources */, DC136582208223F000FC0F60 /* OCBookmark+Extension.swift in Sources */, 3968C882239C54AD00AC28AC /* ReleaseNotesTableViewController.swift in Sources */, @@ -2662,11 +2953,13 @@ DC85572C20513B8C00189B9A /* ServerListTableViewController.swift in Sources */, 233BDEA0204FEFE500C06732 /* AppDelegate.swift in Sources */, 4C51727E22DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift in Sources */, + 4C05D8A5238708D40073EF50 /* MediaUploadStorage.swift in Sources */, 236735A621217C3500E5834A /* MoreViewController.swift in Sources */, 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */, 23957A6D209AFFE8003C8537 /* MoreSettingsSection.swift in Sources */, 4C464BEF2187AF1500D30602 /* PDFThumbnailCollectionViewCell.swift in Sources */, 232B01F62126B10900366FA0 /* MoreStaticTableViewController.swift in Sources */, + 4C96463C238489E4003278B7 /* MediaUploadActivity.swift in Sources */, 6E91F37E21ECA6FD009436D2 /* CopyAction.swift in Sources */, 4CC4A21922FB4F4C00AE7E2C /* MediaUploadQueue.swift in Sources */, 593BAB97209F8A0500023634 /* AppLockManager.swift in Sources */, @@ -2683,7 +2976,6 @@ 23E22BB720C6A5C40024D11E /* UIDevice+UIUserInterfaceIdiom.swift in Sources */, 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */, 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */, - 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */, DC27A19D20CAB602008ACB6C /* FileProviderInterfaceManager.swift in Sources */, 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */, DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, @@ -2707,6 +2999,7 @@ 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */, 39DED4932369879100CCC62C /* ImportFilesController.swift in Sources */, 23C56538212167BE00BD4B47 /* CardTransitionDelegate.swift in Sources */, + 3900348223A100D3000D8510 /* UIApplication+Extension.swift in Sources */, 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */, 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */, 6E3A104D219D6F0100F90C96 /* DuplicateAction.swift in Sources */, @@ -2740,6 +3033,7 @@ DC321261207EB01B00DB171D /* ThemeImage.swift in Sources */, DC7DBA54207FA80C00E7337D /* TVGImage.swift in Sources */, DC33939622E0747400DD3DA4 /* MakeAvailableOfflineAction.swift in Sources */, + 39057AA3233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, 6E586CFE2199A75900F680C4 /* MoveAction.swift in Sources */, DC625148225CEB2C00736874 /* UploadFileAction.swift in Sources */, 394E1FDC233E3750009D2897 /* UnfavoriteAction.swift in Sources */, @@ -2766,6 +3060,7 @@ 233BDEB5204FEFE500C06732 /* OwnCloudTests.swift in Sources */, 59B09E6F21AD61F4007827B8 /* CreateBookmarkTests.swift in Sources */, 59B09E6E21AD61F4007827B8 /* EditBookmarkTests.swift in Sources */, + 39057AA4233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, 59538A0321E4A9C2005E543B /* CreateFolderTests.swift in Sources */, DC869A592153B1F60088977E /* OCMockingManager+SwiftTools.swift in Sources */, EA9337E32226DB070054971F /* SettingsTests.swift in Sources */, @@ -2784,10 +3079,44 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 394A0AF522EEFC2C00603813 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 395E16FA22F03CAF00DE89A1 /* GetFileIntentHandler.swift in Sources */, + 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */, + 397754E223279EED00119FCB /* OCItem+Extension.swift in Sources */, + 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */, + 39E6DE86233CDF1E008DAE04 /* OCItemTracker.swift in Sources */, + 3984F5712319202200DC2639 /* DeletePathItemIntentHandler.swift in Sources */, + 39F689AC22F0206900E63429 /* OCBookmark+Extension.swift in Sources */, + 395E16FF22F172C900DE89A1 /* CreateFolderIntentHandler.swift in Sources */, + 395E16FD22F06A7300DE89A1 /* SaveFileIntentHandler.swift in Sources */, + 39E6DE84233CC39A008DAE04 /* Intents.intentdefinition in Sources */, + 3940C4F02326985B008227AE /* GetAccountIntentHandler.swift in Sources */, + 39A5C3A1231566D9009D9EE3 /* GetFileInfoIntentHandler.swift in Sources */, + 394A0B0522EEFCA300603813 /* GetAccountsIntentHandler.swift in Sources */, + 399A4C1023190ADF0027DDD6 /* PathExistsIntentHandler.swift in Sources */, + 39F689AB22F018C100E63429 /* GetDirectoryListingIntentHandler.swift in Sources */, + 399A4C002317CC460027DDD6 /* AppLockHelper.swift in Sources */, + 3912208223436EB80026C290 /* SortMethod.swift in Sources */, + 399A4C032317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A7137C22E79C6700089423 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 394A0AED22EEE32C00603813 /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 59056CA622414F3C00A18A22 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 39057AA8233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, 59056CAD22414F3C00A18A22 /* ownCloudScreenshotsTests.swift in Sources */, 59056CB422414F8000A18A22 /* SnapshotHelper.swift in Sources */, 59296652224CD1DB0078F13D /* OCBookmarkManager+Tools.swift in Sources */, @@ -2819,6 +3148,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 39057AA7233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, DCC0856C2293F1FD008CC05C /* ownCloudAppTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2855,6 +3185,26 @@ target = 233BDE9B204FEFE500C06732 /* ownCloud */; targetProxy = 233BDEB1204FEFE500C06732 /* PBXContainerItemProxy */; }; + 394A0AFF22EEFC2C00603813 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 394A0AF822EEFC2C00603813 /* ownCloudAppShared */; + targetProxy = 394A0AFE22EEFC2C00603813 /* PBXContainerItemProxy */; + }; + 394A0B0822EEFCD900603813 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 394A0AF822EEFC2C00603813 /* ownCloudAppShared */; + targetProxy = 394A0B0722EEFCD900603813 /* PBXContainerItemProxy */; + }; + 394A0B0C22EEFD2800603813 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ownCloudSDK; + targetProxy = 394A0B0B22EEFD2800603813 /* PBXContainerItemProxy */; + }; + 39A7138622E79C6700089423 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 39A7137F22E79C6700089423 /* ownCloud Intents */; + targetProxy = 39A7138522E79C6700089423 /* PBXContainerItemProxy */; + }; 59056CB022414F3C00A18A22 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 233BDE9B204FEFE500C06732 /* ownCloud */; @@ -2956,6 +3306,15 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */ = { + isa = PBXVariantGroup; + children = ( + 39057AA9233BA7A60008E6C0 /* Base */, + 39057AB1233BA7AE0008E6C0 /* en */, + ); + name = Intents.intentdefinition; + sourceTree = ""; + }; 593A821320C7D4C5000E2A90 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -2976,6 +3335,7 @@ 59AAD98D21F786CC00D15F07 /* pt-PT */, 59AAD98F21F7870B00D15F07 /* th-TH */, 39880BAA233B5236006EA539 /* eu */, + 39954DDF23A39549006B6DC0 /* ar */, ); name = Localizable.strings; sourceTree = ""; @@ -3000,6 +3360,7 @@ 59AAD98C21F786CB00D15F07 /* pt-PT */, 59AAD98E21F7870B00D15F07 /* th-TH */, 39880BB0233B524B006EA539 /* eu */, + 39954DE023A39625006B6DC0 /* ar */, ); name = InfoPlist.strings; sourceTree = ""; @@ -3027,7 +3388,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_SHORT_VERSION = 1.1.2; + APP_SHORT_VERSION = 1.2.1; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -3089,7 +3450,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_SHORT_VERSION = 1.1.2; + APP_SHORT_VERSION = 1.2.1; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -3149,7 +3510,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 149; DEVELOPMENT_TEAM = 4AP2STM4H5; FRAMEWORK_SEARCH_PATHS = "${TARGET_BUILD_DIR}"; GCC_WARN_UNUSED_LABEL = YES; @@ -3178,7 +3539,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 149; DEVELOPMENT_TEAM = 4AP2STM4H5; FRAMEWORK_SEARCH_PATHS = "${TARGET_BUILD_DIR}"; GCC_WARN_UNUSED_LABEL = YES; @@ -3233,6 +3594,114 @@ }; name = Release; }; + 394A0B0322EEFC2C00603813 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = ownCloudAppShared/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudAppShared; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 394A0B0422EEFC2C00603813 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = ownCloudAppShared/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudAppShared; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 39A7138922E79C6700089423 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_ENTITLEMENTS = "ownCloud Intents/ownCloud Intents.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4AP2STM4H5; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "ownCloud Intents/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.owncloud.ios-app.ownCloud-Intents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 39A7138A22E79C6700089423 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_ENTITLEMENTS = "ownCloud Intents/ownCloud Intents.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4AP2STM4H5; + INFOPLIST_FILE = "ownCloud Intents/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.owncloud.ios-app.ownCloud-Intents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 59056CB222414F3C00A18A22 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = A80C5F83C59F9D52F8F64890 /* Pods-ownCloudScreenshotsTests.debug.xcconfig */; @@ -3320,15 +3789,16 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 149; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 149; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -3336,13 +3806,16 @@ ); INFOPLIST_FILE = ownCloudAppFramework/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.2; + INTENTS_CODEGEN_LANGUAGE = Swift; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudApp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -3353,24 +3826,27 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 149; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 149; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ownCloudAppFramework/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.2; + INTENTS_CODEGEN_LANGUAGE = Swift; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudApp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -3380,7 +3856,6 @@ DCC085772293F1FD008CC05C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -3404,7 +3879,6 @@ DCC085782293F1FD008CC05C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -3532,6 +4006,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 394A0B0222EEFC2C00603813 /* Build configuration list for PBXNativeTarget "ownCloudAppShared" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 394A0B0322EEFC2C00603813 /* Debug */, + 394A0B0422EEFC2C00603813 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 39A7138822E79C6700089423 /* Build configuration list for PBXNativeTarget "ownCloud Intents" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 39A7138922E79C6700089423 /* Debug */, + 39A7138A22E79C6700089423 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 59056CB122414F3C00A18A22 /* Build configuration list for PBXNativeTarget "ownCloudScreenshotsTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme index 59b4a46c5..b9aae8e4f 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme @@ -42,6 +42,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -73,24 +82,24 @@ ReferencedContainer = "container:ownCloud.xcodeproj"> + + + + - - - - - - - - + + + + @@ -73,24 +82,24 @@ ReferencedContainer = "container:ownCloud.xcodeproj"> + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme index 6d67db6c3..ff3da31ca 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme @@ -116,6 +116,16 @@ ReferencedContainer = "container:ownCloud.xcodeproj"> + + + + Void) { + func exportVideo(targetURL:URL, type:AVFileType) -> Bool { if self.isExportable { - + let group = DispatchGroup() let preset = AVAssetExportPresetHighestQuality + var compatiblePreset = false + var exportSuccess = false + group.enter() AVAssetExportSession.determineCompatibility(ofExportPreset: preset, with: self, outputFileType: type, completionHandler: { (isCompatible) in - if !isCompatible { - completion(false) - }}) + compatiblePreset = isCompatible + group.leave() + }) - guard let export = AVAssetExportSession(asset: self, presetName: preset) else { - completion(false) - return - } + group.wait() + + if compatiblePreset { + guard let export = AVAssetExportSession(asset: self, presetName: preset) else { + return false + } + // Configure export session + export.outputFileType = type + export.outputURL = targetURL - export.outputFileType = type - export.outputURL = targetURL - export.exportAsynchronously { - completion( export.status == .completed ) + // Start export + group.enter() + export.exportAsynchronously { + exportSuccess = (export.status == .completed) + group.leave() + } + + group.wait() } - } else { - completion(false) + + return exportSuccess } + + return false } } diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index 64a641b58..3dea80ea0 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -26,7 +26,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var serverListTableViewController: ServerListTableViewController? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. var navigationController: UINavigationController? // Set up logging (incl. stderr redirection) and log launch time, app version, build number and commit @@ -37,7 +36,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ThemeStyle.registerDefaultStyles() - serverListTableViewController = ServerListTableViewController(style: UITableView.Style.plain) + serverListTableViewController = ServerListTableViewController(style: .plain) navigationController = ThemeNavigationController(rootViewController: serverListTableViewController!) @@ -82,11 +81,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCExtensionManager.shared.addExtension(FavoriteAction.actionExtension) OCExtensionManager.shared.addExtension(UnfavoriteAction.actionExtension) // OCExtensionManager.shared.addExtension(ScanAction.actionExtension) + if #available(iOS 13.0, *), UIDevice.current.isIpad() { OCExtensionManager.shared.addExtension(DiscardSceneAction.actionExtension) OCExtensionManager.shared.addExtension(OpenSceneAction.actionExtension) } + // Task extensions + OCExtensionManager.shared.addExtension(BackgroundFetchUpdateTaskAction.taskExtension) + OCExtensionManager.shared.addExtension(InstantMediaUploadTaskExtension.taskExtension) + OCExtensionManager.shared.addExtension(PendingMediaUploadTaskExtension.taskExtension) + + // Theming Theme.shared.activeCollection = ThemeCollection(with: ThemeStyle.preferredStyle) // Licenses diff --git a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift index 8b3682d4f..45b422c14 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift @@ -17,6 +17,7 @@ */ import ownCloudSDK +import ownCloudAppShared class MoveAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.move") } diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift index 34360a0c7..833f8b915 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift @@ -57,15 +57,11 @@ class UploadMediaAction: UploadBaseAction { photoAlbumViewController.selectionCallback = {(assets) in self.completed() - guard let rootItem = self.context.items.first else { return } + guard let path = self.context.items.first?.path else { return } - MediaUploadQueue.shared.uploadAssets(assets, with: self.core, at: rootItem, progressHandler: { (progress) in - if progress.isFinished || progress.isCancelled { - self.unpublish(progress: progress) - } else { - self.publish(progress: progress) - } - }) + guard let bookmark = self.core?.bookmark else { return } + + MediaUploadQueue.shared.addUploads(assets, for: bookmark, at: path) } let navigationController = ThemeNavigationController(rootViewController: photoAlbumViewController) diff --git a/ownCloud/Client/ClientActivityCell.swift b/ownCloud/Client/ClientActivityCell.swift index 322868265..a6782f34f 100644 --- a/ownCloud/Client/ClientActivityCell.swift +++ b/ownCloud/Client/ClientActivityCell.swift @@ -70,8 +70,6 @@ class ClientActivityCell: ThemeTableViewCell { statusLabel.rightAnchor.constraint(equalTo: statusCircle.leftAnchor, constant: -20), statusCircle.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor), - statusCircle.topAnchor.constraint(equalTo: self.contentView.topAnchor), - statusCircle.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), statusCircle.widthAnchor.constraint(equalToConstant: 50), statusCircle.rightAnchor.constraint(equalTo: self.contentView.rightAnchor) ]) diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index e201469ec..286c9a03c 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -174,14 +174,17 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega self.tabBar.isTranslucent = false + // Add tab bar icons + Theme.shared.add(tvgResourceFor: "folder") + Theme.shared.add(tvgResourceFor: "owncloud-logo") + Theme.shared.add(tvgResourceFor: "status-flash") + filesNavigationController = ThemeNavigationController() filesNavigationController?.delegate = self filesNavigationController?.navigationBar.isTranslucent = false filesNavigationController?.tabBarItem.title = "Browse".localized filesNavigationController?.tabBarItem.image = Theme.shared.image(for: "folder", size: folderButtonsSize) - Theme.shared.add(tvgResourceFor: "status-flash") - activityViewController = ClientActivityViewController() activityNavigationController = ThemeNavigationController(rootViewController: activityViewController!) activityNavigationController?.tabBarItem.title = "Status".localized @@ -224,17 +227,6 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if MediaUploadQueue.isMediaUploadPendingFlagSet(for: self.bookmark) { - let unfinishedUploadAlert = ThemedAlertController(with: "Warning".localized, - message: "Media upload in the previous session was incomplete since the application was terminated".localized) - self.present(unfinishedUploadAlert, animated: true, completion: nil) - MediaUploadQueue.resetUploadPendingFlag(for: self.bookmark) - } - } - var closeClientCompletionHandler : (() -> Void)? func closeClient(completion: (() -> Void)? = nil) { diff --git a/ownCloud/Client/SortBar.swift b/ownCloud/Client/SortBar.swift index dd3f17506..6e378c842 100644 --- a/ownCloud/Client/SortBar.swift +++ b/ownCloud/Client/SortBar.swift @@ -17,6 +17,7 @@ */ import UIKit +import ownCloudAppShared class SegmentedControl: UISegmentedControl { var oldValue : Int! diff --git a/ownCloud/Client/SortMethodTableViewController.swift b/ownCloud/Client/SortMethodTableViewController.swift index 21c5863d4..b7e0508e3 100644 --- a/ownCloud/Client/SortMethodTableViewController.swift +++ b/ownCloud/Client/SortMethodTableViewController.swift @@ -17,6 +17,7 @@ */ import UIKit +import ownCloudAppShared class SortMethodTableViewController: StaticTableViewController { diff --git a/ownCloud/Client/Viewer/DisplayHostViewController.swift b/ownCloud/Client/Viewer/DisplayHostViewController.swift index 4a9d3be54..12dce8cf3 100644 --- a/ownCloud/Client/Viewer/DisplayHostViewController.swift +++ b/ownCloud/Client/Viewer/DisplayHostViewController.swift @@ -107,6 +107,10 @@ class DisplayHostViewController: UIPageViewController { } NotificationCenter.default.addObserver(self, selector: #selector(handleMediaPlaybackFinished(notification:)), name: MediaDisplayViewController.MediaPlaybackFinishedNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(handlePlayNextMedia(notification:)), name: MediaDisplayViewController.MediaPlaybackNextTrackNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(handlePlayPreviousMedia(notification:)), name: MediaDisplayViewController.MediaPlaybackPreviousTrackNotification, object: nil) } override var childForHomeIndicatorAutoHidden : UIViewController? { @@ -327,6 +331,7 @@ extension DisplayHostViewController: Themeable { } extension DisplayHostViewController { + @objc private func handleMediaPlaybackFinished(notification:Notification) { if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { if let vc = vendNewViewController(from: mediaController, .after) { @@ -334,4 +339,20 @@ extension DisplayHostViewController { } } } + + @objc private func handlePlayNextMedia(notification:Notification) { + if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { + if let vc = vendNewViewController(from: mediaController, .after) { + self.setViewControllers([vc], direction: .forward, animated: false, completion: nil) + } + } + } + + @objc private func handlePlayPreviousMedia(notification:Notification) { + if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { + if let vc = vendNewViewController(from: mediaController, .before) { + self.setViewControllers([vc], direction: .forward, animated: false, completion: nil) + } + } + } } diff --git a/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift b/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift index 692803f45..50bb57b76 100644 --- a/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift +++ b/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift @@ -25,6 +25,8 @@ import MobileCoreServices class MediaDisplayViewController : DisplayViewController { static let MediaPlaybackFinishedNotification = NSNotification.Name("media_playback.finished") + static let MediaPlaybackNextTrackNotification = NSNotification.Name("media_playback.play_next") + static let MediaPlaybackPreviousTrackNotification = NSNotification.Name("media_playback.play_previous") private var playerStatusObservation: NSKeyValueObservation? private var playerItemStatusObservation: NSKeyValueObservation? @@ -40,7 +42,12 @@ class MediaDisplayViewController : DisplayViewController { deinit { playerStatusObservation?.invalidate() playerItemStatusObservation?.invalidate() - NotificationCenter.default.removeObserver(self) + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil) } override func viewDidLoad() { @@ -195,7 +202,8 @@ class MediaDisplayViewController : DisplayViewController { commandCenter.playCommand.addTarget { [weak self] _ in if let player = self?.player { if player.rate == 0.0 { - player.play() + player.play() + self?.updateNowPlayingTimeline() return .success } } @@ -208,6 +216,7 @@ class MediaDisplayViewController : DisplayViewController { if let player = self?.player { if player.rate == 1.0 { player.pause() + self?.updateNowPlayingTimeline() return .success } } @@ -222,9 +231,10 @@ class MediaDisplayViewController : DisplayViewController { let time = player.currentTime() + CMTime(seconds: 10.0, preferredTimescale: 1) player.seek(to: time) { (finished) in if finished { - MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self?.playerItem?.currentTime().seconds + self?.updateNowPlayingTimeline() } } + return .success } return .commandFailed } @@ -236,20 +246,70 @@ class MediaDisplayViewController : DisplayViewController { let time = player.currentTime() - CMTime(seconds: 10.0, preferredTimescale: 1) player.seek(to: time) { (finished) in if finished { - MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self?.playerItem?.currentTime().seconds + self?.updateNowPlayingTimeline() } } + return .success } return .commandFailed } - - // Disable seek commands - commandCenter.seekForwardCommand.isEnabled = false - commandCenter.seekBackwardCommand.isEnabled = false + + // TODO: Skip controls are useful for podcasts but not so much for music. + // Disable them for now but keep the implementation of command handlers + commandCenter.skipForwardCommand.isEnabled = false + commandCenter.skipBackwardCommand.isEnabled = false + + // Configure next / previous track buttons according to number of items to be played + var enableNextTrackCommand = false + var enablePreviousTrackCommand = false + + if let itemIndex = self.itemIndex { + if itemIndex > 0 { + enablePreviousTrackCommand = true + } + + if let displayHostController = self.parent as? DisplayHostViewController, let items = displayHostController.items { + enableNextTrackCommand = itemIndex < (items.count - 1) + } + } + + commandCenter.nextTrackCommand.isEnabled = enableNextTrackCommand + commandCenter.previousTrackCommand.isEnabled = enablePreviousTrackCommand + + // Add handler for seek forward command + commandCenter.nextTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + if let player = self?.player { + player.pause() + OnMainThread { + NotificationCenter.default.post(name: MediaDisplayViewController.MediaPlaybackNextTrackNotification, object: nil) + } + return .success + } + return .commandFailed + } + + // Add handler for seek backward command + commandCenter.previousTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + if let player = self?.player { + player.pause() + OnMainThread { + NotificationCenter.default.post(name: MediaDisplayViewController.MediaPlaybackPreviousTrackNotification, object: nil) + } + return .success + } + return .commandFailed + } } + private func updateNowPlayingTimeline() { + + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.playerItem?.currentTime().seconds + + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.player?.rate + } + private func updateNowPlayingInfoCenter() { - guard let player = self.player else { return } + guard let player = self.player else { return } guard let playerItem = self.playerItem else { return } var nowPlayingInfo = [String : Any]() @@ -258,15 +318,18 @@ class MediaDisplayViewController : DisplayViewController { nowPlayingInfo[MPMediaItemPropertyArtist] = mediaItemArtist nowPlayingInfo[MPNowPlayingInfoPropertyCurrentPlaybackDate] = self.playerItem?.currentDate() nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = source + nowPlayingInfo[MPNowPlayingInfoPropertyCurrentPlaybackDate] = playerItem.currentDate() nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playerItem.currentTime().seconds - nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem.asset.duration.seconds - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem.asset.duration.seconds if mediaItemArtwork != nil { nowPlayingInfo[MPMediaItemPropertyArtwork] = mediaItemArtwork } MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + + updateNowPlayingTimeline() } } diff --git a/ownCloud/FileLists/FileListTableViewController.swift b/ownCloud/FileLists/FileListTableViewController.swift index ebca48488..22836125c 100644 --- a/ownCloud/FileLists/FileListTableViewController.swift +++ b/ownCloud/FileLists/FileListTableViewController.swift @@ -226,7 +226,7 @@ class FileListTableViewController: UITableViewController, ClientItemCellDelegate } } - func open(item: OCItem, animated: Bool, pushViewController: Bool = true) -> ClientQueryViewController? { + @discardableResult func open(item: OCItem, animated: Bool, pushViewController: Bool = true) -> ClientQueryViewController? { if let core = self.core { if #available(iOS 13.0, *) { if let tabBarController = self.tabBarController as? ClientRootViewController { diff --git a/ownCloud/FileLists/QueryFileListTableViewController.swift b/ownCloud/FileLists/QueryFileListTableViewController.swift index c2c4bbd96..30d4bfcea 100644 --- a/ownCloud/FileLists/QueryFileListTableViewController.swift +++ b/ownCloud/FileLists/QueryFileListTableViewController.swift @@ -19,6 +19,7 @@ import UIKit import ownCloudSDK import ownCloudApp +import ownCloudAppShared class QueryFileListTableViewController: FileListTableViewController, SortBarDelegate, OCQueryDelegate, UISearchResultsUpdating { var query : OCQuery @@ -201,7 +202,7 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele func queryHasChangesAvailable(_ query: OCQuery) { queryRefreshRateLimiter.runRateLimitedBlock { - query.requestChangeSet(withFlags: OCQueryChangeSetRequestFlag(rawValue: 0)) { (query, changeSet) in + query.requestChangeSet(withFlags: .onlyResults) { (query, changeSet) in OnMainThread { if query.state.isFinal { OnMainThread { diff --git a/ownCloud/Import/ImportFilesController.swift b/ownCloud/Import/ImportFilesController.swift index af123892d..24cbdd85a 100644 --- a/ownCloud/Import/ImportFilesController.swift +++ b/ownCloud/Import/ImportFilesController.swift @@ -129,7 +129,7 @@ extension ImportFilesController { if bookmarks.count > 0 { let moreViewController = self.cardViewController(for: self.localCopyURL ?? self.url) - if let delegateWindow = UIApplication.shared.delegate?.window, let window = delegateWindow { + if let window = UIApplication.shared.currentWindow() { let viewController = window.rootViewController if let navigationController = viewController as? UINavigationController, let viewController = navigationController.visibleViewController { OnMainThread { @@ -159,8 +159,8 @@ extension ImportFilesController { let pickerNavigationController = ThemeNavigationController(rootViewController: directoryPickerViewController) pickerNavigationController.modalPresentationStyle = .formSheet - if let window = UIApplication.shared.delegate?.window { - let viewController = window!.rootViewController + if let window = UIApplication.shared.currentWindow() { + let viewController = window.rootViewController if let navCon = viewController as? UINavigationController, let viewController = navCon.visibleViewController { viewController.present(pickerNavigationController, animated: true) } else { diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index 6b299ec5d..2b07f1380 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -18,6 +18,7 @@ import UIKit import ownCloudSDK +import ownCloudAppShared import MobileCoreServices extension ServerListTableViewController { diff --git a/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift b/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift new file mode 100644 index 000000000..f83b87da1 --- /dev/null +++ b/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift @@ -0,0 +1,60 @@ +// +// MediaUploadActivity.swift +// ownCloud +// +// Created by Michael Neuwert on 19.11.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import ownCloudSDK + +class MediaUploadActivity : OCActivity { + + var isCancelled: Bool { + if let progress = self.progress { + return progress.isCancelled + } + return false + } + + init(identifier: String, assetCount:Int) { + super.init(identifier: identifier) + self.isCancellable = true + self.localizedDescription = "Media import".localized + + if assetCount <= 0 { + self.progress = Progress.indeterminate() + } else { + self.progress = Progress(totalUnitCount: Int64(assetCount)) + self.updateStatusMessage() + } + } + + public func updateAfterSingleFinishedUpload() { + guard let progress = self.progress else { return } + guard progress.isIndeterminate == false else { return } + + self.progress?.completedUnitCount += 1 + self.updateStatusMessage() + } + + // MARK: - Private helper methods + + private func updateStatusMessage() { + if let progress = self.progress, progress.isIndeterminate == false { + let total = progress.totalUnitCount + let current = progress.completedUnitCount + self.localizedStatusMessage = String(format: "%@ of %@".localized, "\(current)", "\(total)") + } + } +} diff --git a/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift b/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift index 1ad4b493e..c3efc2dcc 100644 --- a/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift +++ b/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift @@ -21,114 +21,306 @@ import ownCloudSDK import Photos import MobileCoreServices -class MediaUploadQueue { +class MediaUploadQueue : OCActivitySource { - private let uploadSerialQueue = DispatchQueue(label: "com.owncloud.upload.queue", target: DispatchQueue.global(qos: .background)) + private var uploadActivity: MediaUploadActivity? - static let shared = MediaUploadQueue() + static var shared = MediaUploadQueue() - static let UploadPendingKey = OCKeyValueStoreKey(rawValue: "com.owncloud.upload.queue.upload-pending-flag") + // MARK: - OCActivitySource protocol implementation - private static var uploadStarted = false + func provideActivity() -> OCActivity { + return self.uploadActivity! + } - func uploadAssets(_ assets:[PHAsset], with core:OCCore?, at rootItem:OCItem, progressHandler:((Progress) -> Void)? = nil, assetUploadCompletion:((_ asset:PHAsset?, _ finished:Bool) -> Void)? = nil ) { + var activityIdentifier: String { + if let activity = self.uploadActivity { + return activity.identifier + } else { + return "" + } + } - let backgroundTask = OCBackgroundTask(name: "UploadMediaAction", expirationHandler: { (bgTask) in - Log.warning("UploadMediaAction background task expired") - bgTask.end() - }).start() + // MARK: - Public interface - let queue = DispatchQueue.global(qos: .userInitiated) + func addUpload(_ asset:PHAsset, for bookmark:OCBookmark, at path:String) { - weak var weakCore = core + bookmark.modifyMediaUploadStorage { (storage) -> MediaUploadStorage in + storage.addJob(with: asset.localIdentifier, targetPath: path) + return storage + } - if weakCore != nil { - let vault : OCVault = OCVault(bookmark: weakCore!.bookmark) - let flag = NSNumber(value: true) - vault.keyValueStore?.storeObject(flag, forKey: MediaUploadQueue.UploadPendingKey) + self.setNeedsScheduling(in: bookmark) + } - MediaUploadQueue.uploadStarted = true + func addUploads(_ assets:[PHAsset], for bookmark:OCBookmark, at path:String) { + bookmark.modifyMediaUploadStorage { (storage) -> MediaUploadStorage in + for asset in assets { + storage.addJob(with: asset.localIdentifier, targetPath: path) + } + return storage } + self.setNeedsScheduling(in: bookmark) + } - queue.async { + private var _needsSchedulingCountByBookmarkUUID : [UUID : Int] = [:] + func setNeedsScheduling(in bookmark: OCBookmark) { + // Increment counter by one + _needsSchedulingCountByBookmarkUUID[bookmark.uuid] = (_needsSchedulingCountByBookmarkUUID[bookmark.uuid] ?? 0) + 1 - guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } + // Schedule right away. If it's already busy, it'll return quickly. If not, it'll schedule. + self.scheduleUploads(in: bookmark) + } - var prefferedMediaOutputFormats = [String]() - if userDefaults.convertHeic { - prefferedMediaOutputFormats.append(String(kUTTypeJPEG)) - } - if userDefaults.convertVideosToMP4 { - prefferedMediaOutputFormats.append(String(kUTTypeMPEG4)) + func scheduleUploads(in bookmark:OCBookmark) { + + var uploadStorageAlreadyProcessing = false + var uploadStorageQueueEmpty = false + var needsSchedulingCountAtEntry = _needsSchedulingCountByBookmarkUUID[bookmark.uuid] + + // Avoid race conditions by performing checks and modifications atomically + bookmark.modifyMediaUploadStorage { (mediaUploadStorage) -> MediaUploadStorage in + // First check if there are any media upload jobs stored + if mediaUploadStorage.jobCount == 0 { + uploadStorageQueueEmpty = true + } else { + // Check if upload queue processing can be started and no-one else is processing it + if mediaUploadStorage.processing != nil { + // Found OCProcessSession instance -> check if it is valid though + if OCProcessManager.shared.isSessionValid(mediaUploadStorage.processing!, usingThoroughChecks: true) { + // If the process session is valid, may be it is being used by running extension --> bail out + uploadStorageAlreadyProcessing = true + } else { + // Remove invalid session + mediaUploadStorage.processing = nil + } + } + + if mediaUploadStorage.processing == nil { + // Mark the queue as being processed + mediaUploadStorage.processing = OCProcessManager.shared.processSession + } } - let uploadGroup = DispatchGroup() - var uploadFailed = false + return mediaUploadStorage + } - for asset in assets { - if uploadFailed == false { - uploadGroup.enter() - self.uploadSerialQueue.async { - if weakCore != nil { - weakCore!.perform(inRunningCore: { (runningCoreCompletion) in - asset.upload(with: weakCore!, at: rootItem, preferredFormats: prefferedMediaOutputFormats, completionHandler: { (item, _) in - if item == nil { - uploadFailed = true - } else { - assetUploadCompletion?(asset, false) + if uploadStorageAlreadyProcessing || uploadStorageQueueEmpty { + // Already processing or nothing to do + return + } + + // Request a core for the passed bookmark + OCCoreManager.shared.requestCore(for: bookmark, setup:nil, completionHandler: {(core, _) in + + if let core = core { + + // This method is called when media import is finished or cancelled either by published activity or through unrecoverable error + func finalizeImport() { + self.unpublishImportActivity(for: core) + + OCCoreManager.shared.returnCore(for: bookmark, completionHandler: nil) + + // Mark the media upload storage as not in use anymore + bookmark.modifyMediaUploadStorage { (mediaUploadStorage) in + mediaUploadStorage.processing = nil + return mediaUploadStorage + } + + // Check if .setNeedsScheduling() has been called since starting the scheduling: + // since any new entries added to the queue after scheduling has started will not be handled, + // it's important to start scheduling again if any change to the queue has been performed since + if needsSchedulingCountAtEntry != self._needsSchedulingCountByBookmarkUUID[bookmark.uuid] { + self.scheduleUploads(in: bookmark) + } + } + + // Publish activity including number of jobs to be processed + guard let uploadStorage = bookmark.mediaUploadStorage else { return } + + self.publishImportActivity(for: core, itemCount: uploadStorage.jobCount) + + // Create background task used to continue media upload in the background + let backgroundTask = OCBackgroundTask(name: "com.owncloud.media-upload-task", expirationHandler: { (bgTask) in + Log.warning("UploadMediaAction background task expired") + bgTask.end() + }).start() + + let queue = DispatchQueue.global(qos: .background) + let importGroup = DispatchGroup() + + queue.async { + // Make copies to avoid side effects of caching that KVS might perform + let assetIDQueue : [String] = uploadStorage.queue + let jobsByAssetID : [String : [MediaUploadJob]] = uploadStorage.jobs + + // Iterate over PHObject local asset IDs + for assetId in assetIDQueue { + + if let assetJobs = jobsByAssetID[assetId] { + // Iterate over jobs associated with asset ID + for job in assetJobs { + + // Check if the import activity has been cancelled + if self.isImportActivityCancelled() { + // Remove all stored jobs + bookmark.modifyMediaUploadStorage { (_) -> MediaUploadStorage in + return MediaUploadStorage() + } + + finalizeImport() + return + } + + // Skip jobs for which local item IDs are valid and known in the scope of the current bookmark + if let localID = job.scheduledUploadLocalID { + if let existingItem = self.findItem(in: core, for: localID as String) { + // If item is found and it's not a placeholder, upload was finished + if existingItem.isPlaceholder == false { + // Now upload is done and the job can be removed completely + if let itemPath = existingItem.path { + bookmark.modifyMediaUploadStorage { (storage) -> MediaUploadStorage in + storage.removeJob(with: assetId, targetPath: itemPath) + return storage + } + } + } + // Otherwise if isPlaceholder property is true, then upload is still ongoing, just skip it here + continue + } + } + + // Found a job which requires an import operation + if let asset = self.fetchAsset(with: assetId), let path = job.targetPath { + + importGroup.enter() + + // Convert target path to the OCItem object + var tracking = core.trackItem(atPath: path) { (_, item, isInitial) in + + if isInitial == true { + + defer { + importGroup.leave() + } + + if let rootItem = item { + // Perform asset import + if let itemLocalId = self.importAsset(asset: asset, using: core, at: rootItem, uploadCompletion: { + + // Now upload is done and the job can be removed completely + bookmark.modifyMediaUploadStorage { (storage) -> MediaUploadStorage in + storage.removeJob(with: assetId, targetPath: path) + return storage + } + + })?.localID as OCLocalID? { + + // Update import activity + self.updateActivityAfterFinishedImport(for: core) + + // Update media upload storage object + bookmark.modifyMediaUploadStorage { (storage) in + storage.update(localItemID: itemLocalId, assetId: assetId, targetPath: path) + return storage + } + } + } + } } - runningCoreCompletion() - uploadGroup.leave() - }, progressHandler: { (progress) in - progressHandler?(progress) - }) - }, withDescription: "Uploading \(assets.count) photo assets") - - } else { - uploadGroup.leave() - // Core reference became nil - uploadFailed = true + } + + importGroup.wait() + } } } - // Avoid submitting to many jobs simultaneously to reduce memory pressure - _ = uploadGroup.wait() - } else { - // Escape on first failed download - break + // Wait until all to-be-uploaded assets are processed + importGroup.notify(queue: queue, execute: { + // Finish background task + backgroundTask?.end() + finalizeImport() + }) } } + }) + } - uploadGroup.notify(queue: queue, execute: { - if weakCore != nil { - MediaUploadQueue.resetUploadPendingFlag(for: weakCore!.bookmark) - } - MediaUploadQueue.uploadStarted = false - backgroundTask?.end() - assetUploadCompletion?(nil, true) - }) + // MARK: - Private helper methods + + private func findItem(in core:OCCore, for localID:String) -> OCItem? { + guard let database = core.vault.database else { return nil } + var foundItem: OCItem? + + let semaphore = DispatchSemaphore(value: 0) + + database.retrieveCacheItem(forLocalID: localID, completionHandler: { (_, _, _, item) in + foundItem = item + semaphore.signal() + }) + + semaphore.wait() + + return foundItem + } + + private func fetchAsset(with assetID:String) -> PHAsset? { + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil) + if fetchResult.count > 0 { + return fetchResult.object(at: 0) } + return nil } - class func isMediaUploadPendingFlagSet(for bookmark:OCBookmark) -> Bool { - let vault : OCVault = OCVault(bookmark: bookmark) - if vault.keyValueStore?.readObject(forKey: MediaUploadQueue.UploadPendingKey) != nil { - return true - } else { - return false + private func importAsset(asset:PHAsset, using core:OCCore, at rootItem:OCItem, uploadCompletion: @escaping () -> Void) -> OCItem? { + + // Determine the list of preferred media formats + var prefferedMediaOutputFormats = [String]() + + if let userDefaults = OCAppIdentity.shared.userDefaults { + if userDefaults.convertHeic { + prefferedMediaOutputFormats.append(String(kUTTypeJPEG)) + } + if userDefaults.convertVideosToMP4 { + prefferedMediaOutputFormats.append(String(kUTTypeMPEG4)) + } } + + if let result = asset.upload(with: core, at: rootItem, preferredFormats: prefferedMediaOutputFormats, progressHandler: nil, uploadCompleteHandler: { + uploadCompletion() + }) { + if let error = result.1 { + Log.error("Asset upload failed with error \(error)") + } + + return result.0 + } + + return nil } - class func resetUploadPendingFlag(for bookmark:OCBookmark) { - let vault : OCVault = OCVault(bookmark: bookmark) - vault.keyValueStore?.storeObject(nil, forKey: MediaUploadQueue.UploadPendingKey) + // MARK: - Activity management + + private func publishImportActivity(for core:OCCore, itemCount:Int) { + let activityId = "MediaUploadQueue:\(UUID())" + self.uploadActivity = MediaUploadActivity(identifier: activityId, assetCount: itemCount) + core.activityManager.update(OCActivityUpdate.publishingActivity(for: self)) } - class func shallShowUploadUnfinishedWarning(for bookmark:OCBookmark) -> Bool { - if !MediaUploadQueue.uploadStarted && isMediaUploadPendingFlagSet(for: bookmark) { - return true - } else { - return false + private func updateActivityAfterFinishedImport(for core:OCCore) { + self.uploadActivity?.updateAfterSingleFinishedUpload() + core.activityManager.update(OCActivityUpdate.updatingActivity(for: self)) + } + + private func unpublishImportActivity(for core:OCCore) { + core.activityManager.update(OCActivityUpdate.unpublishActivity(for: self)) + } + + private func isImportActivityCancelled() -> Bool { + if let activity = self.uploadActivity { + return activity.isCancelled } + return false } + } diff --git a/ownCloud/PhotoKit Extensions/MediaUploadStorage.swift b/ownCloud/PhotoKit Extensions/MediaUploadStorage.swift new file mode 100644 index 000000000..60d2a0c71 --- /dev/null +++ b/ownCloud/PhotoKit Extensions/MediaUploadStorage.swift @@ -0,0 +1,151 @@ +// +// File.swift +// ownCloud +// +// Created by Michael Neuwert on 21.11.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +import Foundation +import Photos +import ownCloudSDK + +class MediaUploadJob : NSObject, NSSecureCoding { + + var targetPath: String? + var scheduledUploadLocalID: OCLocalID? + + static var supportsSecureCoding: Bool { + return true + } + + func encode(with coder: NSCoder) { + coder.encode(targetPath, forKey: "targetPath") + coder.encode(scheduledUploadLocalID, forKey: "scheduledUploadLocalID") + } + + required init?(coder: NSCoder) { + self.targetPath = coder.decodeObject(forKey: "targetPath") as? String + self.scheduledUploadLocalID = coder.decodeObject(forKey: "scheduledUploadLocalID") as? OCLocalID + } + + init(_ path:String) { + self.targetPath = path + } +} + +class MediaUploadStorage : NSObject, NSSecureCoding { + + var queue: [String] + var jobs: [String : [MediaUploadJob]] + var processing: OCProcessSession? + + static var supportsSecureCoding: Bool { + return true + } + + var jobCount: Int { + jobs.reduce(0) {$0 + $1.value.count } + } + + func encode(with coder: NSCoder) { + coder.encode(queue, forKey: "queue") + coder.encode(jobs, forKey: "jobs") + coder.encode(processing, forKey: "processing") + } + + required init?(coder: NSCoder) { + let storedQueue = coder.decodeObject(forKey: "queue") as? [String] + self.queue = storedQueue != nil ? storedQueue! : [String]() + + let storedJobs = coder.decodeObject(forKey: "jobs") as? [String : [MediaUploadJob]] + jobs = storedJobs != nil ? storedJobs! : [String : [MediaUploadJob]]() + + processing = coder.decodeObject(forKey: "processing") as? OCProcessSession + } + + override init() { + queue = [String]() + jobs = [String : [MediaUploadJob]]() + } + + func addJob(with assetID:String, targetPath:String) { + if !queue.contains(assetID) { + self.queue.append(assetID) + } + var existingJobs: [MediaUploadJob] = jobs[assetID] != nil ? jobs[assetID]! : [MediaUploadJob]() + if existingJobs.filter({$0.targetPath == targetPath}).count == 0 { + existingJobs.append(MediaUploadJob(targetPath)) + } + jobs[assetID] = existingJobs + } + + func removeJob(with assetID:String, targetPath:String) { + if let remainingJobs = jobs[assetID]?.filter({$0.targetPath != targetPath}) { + jobs[assetID] = remainingJobs + if remainingJobs.count == 0 { + if let assetIdQueueIndex = queue.firstIndex(of: assetID) { + queue.remove(at: assetIdQueueIndex) + } + } + } + } + + func update(localItemID:OCLocalID, assetId:String, targetPath:String) { + jobs[assetId]?.filter({$0.targetPath == targetPath}).first?.scheduledUploadLocalID = localItemID + } +} + +typealias MediaUploadStorageModifier = (_ storage:MediaUploadStorage) -> MediaUploadStorage + +extension OCBookmark { + private static let MediaUploadStorageKey = OCKeyValueStoreKey(rawValue: "com.owncloud.media-upload-storage") + + // + // NOTE: Deriving KV store from bookmark rather than from OCCore since it simplifies adding upload jobs + // significantly without a need to initialize full blown core for that + // + + var kvStore : OCKeyValueStore? { + let vault : OCVault = OCVault(bookmark: self) + if let store = vault.keyValueStore { + + // Check if NSCoding-compatible classes are not yet registered? + if store.registeredClasses(forKey: OCBookmark.MediaUploadStorageKey) == nil { + + // This weird trickery is required since Set can't be created directly + if let classSet = NSSet(array: [MediaUploadStorage.self, + MediaUploadJob.self, + OCProcessSession.self, + NSArray.self, + NSDictionary.self, + NSString.self]) as? Set { + + store.registerClasses(classSet, forKey: OCBookmark.MediaUploadStorageKey) + } + } + return store + } + return nil + } + + func modifyMediaUploadStorage(with modifier: @escaping MediaUploadStorageModifier) { + self.kvStore?.updateObject(forKey: OCBookmark.MediaUploadStorageKey, usingModifier: { (value, changesMadePtr) -> Any? in + var storage = value as? MediaUploadStorage + + if storage == nil { + storage = MediaUploadStorage() + } + + storage = modifier(storage!) + + changesMadePtr.pointee = true + + return storage + }) + } + + var mediaUploadStorage : MediaUploadStorage? { + return self.kvStore?.readObject(forKey: OCBookmark.MediaUploadStorageKey) as? MediaUploadStorage + } +} diff --git a/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift b/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift index e7695f1dd..dcd8a1495 100644 --- a/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift +++ b/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift @@ -28,10 +28,11 @@ extension PHAsset { - parameter preferredFormats: Array of UTI identifiers describing desired output formats - parameter completionHandler: Completion handler called after the media file is imported into the core and placeholder item is created. - parameter progressHandler: Receives progress of the at the moment running activity + - parameter uploadCompleteHandler: Called when core reports that upload is done */ - func upload(with core:OCCore?, at rootItem:OCItem, preferredFormats:[String]? = nil, completionHandler:@escaping (_ item:OCItem?, _ error:Error?) -> Void, progressHandler:((_ progress:Progress) -> Void)? = nil) { + func upload(with core:OCCore?, at rootItem:OCItem, preferredFormats:[String]? = nil, progressHandler:((_ progress:Progress) -> Void)? = nil, uploadCompleteHandler:(() -> Void)? = nil) -> (OCItem?, Error?)? { - func performUpload(sourceURL:URL, copySource:Bool) { + func performUpload(sourceURL:URL, copySource:Bool) -> (OCItem?, Error?)? { @discardableResult func removeSourceFile() -> Bool { do { @@ -43,6 +44,7 @@ extension PHAsset { } var uploadProgress: Progress? + var importResult:(OCItem?, Error?)? // Sometimes if the image was edited, the name is FullSizeRender.jpg but it is stored in the subfolder // in the PhotoLibrary which is named after original image @@ -58,6 +60,9 @@ extension PHAsset { } } + // Synchronously import media file into the OCCore and schedule upload + let importSemaphore = DispatchSemaphore(value: 0) + uploadProgress = sourceURL.upload(with: core, at: rootItem, alternativeName: fileName, @@ -72,17 +77,23 @@ extension PHAsset { } else { Log.debug(tagged: ["MEDIA_UPLOAD"], "Finished uploading asset ID \(self.localIdentifier)") } - completionHandler(item, error) + importResult = (item, error) + importSemaphore.signal() }, completionHandler: { (_, _) in if uploadProgress != nil { progressHandler?(uploadProgress!) } + uploadCompleteHandler?() }) if uploadProgress != nil { progressHandler?(uploadProgress!) } + + importSemaphore.wait() + + return importResult } Log.debug(tagged: ["MEDIA_UPLOAD"], "Prepare uploading asset ID \(self.localIdentifier), type:\(self.mediaType), subtypes:\(self.mediaSubtypes), sourceType:\(self.sourceType), creationDate:\(String(describing: self.creationDate)), modificationDate:\(String(describing: self.modificationDate)), favorite:\(self.isFavorite), hidden:\(self.isHidden)") @@ -95,23 +106,30 @@ extension PHAsset { let contentInputOptions = PHContentEditingInputRequestOptions() contentInputOptions.isNetworkAccessAllowed = true + var supportedConversionFormats = Set() + var assetUTI: String? + var assetURL: URL? + var avAsset:AVAsset? + var uploadResult:(OCItem?, Error?)? + + // Synchronously retrieve asset content + let semaphore = DispatchSemaphore(value: 0) _ = autoreleasepool { self.requestContentEditingInput(with: contentInputOptions) { (contentInput, requestInfo) in - - var supportedConversionFormats = Set() + if let error = requestInfo[PHContentEditingInputErrorKey] as? NSError { + Log.error(tagged: ["MEDIA_UPLOAD"], "Requesting content for asset ID \(self.localIdentifier) with error \(String(describing: error))") + } if let input = contentInput { - // Determine the correct source URL based on media type - var assetUTI: String? - var assetURL: URL? switch self.mediaType { case .image: assetURL = input.fullSizeImageURL assetUTI = input.uniformTypeIdentifier supportedConversionFormats.insert(String(kUTTypeJPEG)) case .video: - assetURL = (input.audiovisualAsset as? AVURLAsset)?.url + avAsset = input.audiovisualAsset + assetURL = (avAsset as? AVURLAsset)?.url assetUTI = PHAssetResource.assetResources(for: self).first?.uniformTypeIdentifier supportedConversionFormats.insert(String(kUTTypeMPEG4)) default: @@ -119,69 +137,72 @@ extension PHAsset { } Log.debug(tagged: ["MEDIA_UPLOAD"], "Preparing to export asset ID \(self.localIdentifier), URL: \(String(describing: assetURL)), UTI: \(String(describing: assetUTI))") + } - guard let url = assetURL else { return } - - let fileName = url.lastPathComponent - - // Check if the conversion was requested and current media format is not found in the list of requested formats - if let formats = preferredFormats, formats.count > 0 { - if assetUTI != nil, !formats.contains(assetUTI!) { - // Conversion is required - if let outputFormat = formats.first(where: { supportedConversionFormats.contains($0) }) { - - switch (self.mediaType, outputFormat) { - case (.video, String(kUTTypeMPEG4)): - if let avAsset = input.audiovisualAsset { - let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("mp4") - avAsset.exportVideo(targetURL: localURL, type: .mp4, completion: { (exportSuccess) in - if exportSuccess { - performUpload(sourceURL: localURL, copySource: false) - } else { - Log.error(tagged: ["MEDIA_UPLOAD"], "Video export failed for asset ID \(self.localIdentifier)") - completionHandler(nil, NSError(ocError: .internal)) - } - }) - } - case (.image, String(kUTTypeJPEG)): - let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("jpg") - var imageConverted = false - - if let image = CIImage(contentsOf: assetURL!) { - imageConverted = image.convert(targetURL: localURL, outputFormat: .JPEG) - } - - if imageConverted { - performUpload(sourceURL: localURL, copySource: false) - } else { - Log.error(tagged: ["MEDIA_UPLOAD"], "CIImage conversion failed for \(self.localIdentifier)") - completionHandler(nil, NSError(ocError: .internal)) - } - default: - break - } - + semaphore.signal() + } + } + semaphore.wait() + + guard let url = assetURL else { return (nil, nil) } + let fileName = url.lastPathComponent + var exportedAssetURL: URL? + var conversionError: Error? + + // Check if the conversion was requested and current media format is not found in the list of requested formats + if let formats = preferredFormats, formats.count > 0 { + if assetUTI != nil, !formats.contains(assetUTI!) { + // Conversion is required + if let outputFormat = formats.first(where: { supportedConversionFormats.contains($0) }) { + + switch (self.mediaType, outputFormat) { + case (.video, String(kUTTypeMPEG4)): + if let avAsset = avAsset { + // Export video + let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("mp4") + if avAsset.exportVideo(targetURL: localURL, type: .mp4) { + exportedAssetURL = localURL } else { - Log.error(tagged: ["MEDIA_UPLOAD"], "Format mismatch for asset ID \(self.localIdentifier)") - completionHandler(nil, NSError(ocError: .internal)) + Log.error(tagged: ["MEDIA_UPLOAD"], "Video export failed for asset ID \(self.localIdentifier)") + conversionError = NSError(ocError: .internal) } + } + case (.image, String(kUTTypeJPEG)): + // Convert image to JPEG format + let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("jpg") + var imageConverted = false + + if let image = CIImage(contentsOf: assetURL!) { + imageConverted = image.convert(targetURL: localURL, outputFormat: .JPEG) + } + + if imageConverted { + exportedAssetURL = localURL } else { - performUpload(sourceURL: url, copySource: true) + Log.error(tagged: ["MEDIA_UPLOAD"], "CIImage conversion failed for \(self.localIdentifier)") + conversionError = NSError(ocError: .internal) } - } else { - performUpload(sourceURL: url, copySource: true) + default: + break } + } else { + Log.debug(tagged: ["MEDIA_UPLOAD"], "No conversion format match for asset ID \(self.localIdentifier), URL: \(String(describing: assetURL)), UTI: \(String(describing: assetUTI))") + } + } + } - } else { - // If no content was returned check request info dictionary - let error = requestInfo[PHContentEditingInputErrorKey] as? NSError - - Log.error(tagged: ["MEDIA_UPLOAD"], "Requesting content for asset ID \(self.localIdentifier) with error \(String(describing: error))") + // Bail out if the conversion has failed + if conversionError != nil { + return (nil, conversionError) + } - completionHandler(nil, error) - } - } + // Perform actual upload + if exportedAssetURL != nil { + uploadResult = performUpload(sourceURL: exportedAssetURL!, copySource: false) + } else { + uploadResult = performUpload(sourceURL: url, copySource: true) } - } + return uploadResult + } } diff --git a/ownCloud/Resources/Assets.xcassets/cloud-available-offline.imageset/cloud-available-offline@2x.png b/ownCloud/Resources/Assets.xcassets/cloud-available-offline.imageset/cloud-available-offline@2x.png index 923124e45..32c9dbb98 100644 Binary files a/ownCloud/Resources/Assets.xcassets/cloud-available-offline.imageset/cloud-available-offline@2x.png and b/ownCloud/Resources/Assets.xcassets/cloud-available-offline.imageset/cloud-available-offline@2x.png differ diff --git a/ownCloud/Resources/Info.plist b/ownCloud/Resources/Info.plist index 9dcb7f725..38d34acd1 100644 --- a/ownCloud/Resources/Info.plist +++ b/ownCloud/Resources/Info.plist @@ -36,7 +36,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 144 + 149 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -66,6 +66,15 @@ com.owncloud.ios-app.openAccount com.owncloud.ios-app.openItem + CreateFolderIntent + DeletePathItemIntent + GetAccountIntent + GetAccountsIntent + GetDirectoryListingIntent + GetFileInfoIntent + GetFileIntent + PathExistsIntent + SaveFileIntent OCAppGroupIdentifier group.$(PRODUCT_BUNDLE_IDENTIFIER) diff --git a/ownCloud/Resources/ar.lproj/InfoPlist.strings b/ownCloud/Resources/ar.lproj/InfoPlist.strings new file mode 100644 index 000000000..4735c62da --- /dev/null +++ b/ownCloud/Resources/ar.lproj/InfoPlist.strings @@ -0,0 +1,22 @@ +/* + InfoPlist.strings + ownCloud + + Created by Javier Gonzalez on 06/06/2018. + Copyright © 2018 ownCloud GmbH. All rights reserved. +*/ + +/* + * Copyright (C) 2018, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +"NSFaceIDUsageDescription" = "إلغاء قفل التطبيق دون كتابة الرمز المروري."; +"NSAppleMusicUsageDescription" = "هذا الإذن مطلوب لتحميل ملفات الوسائط من الخادم."; +"NSPhotoLibraryAddUsageDescription" = "هذا الإذن مطلوب لتحميل الصور والفيديوهات من مكتبة الصور."; +"NSPhotoLibraryUsageDescription" = "هذا الإذن مطلوب لتحميل الصور والفيديوهات من مكتبة الصور."; diff --git a/ownCloud/Resources/ar.lproj/Localizable.strings b/ownCloud/Resources/ar.lproj/Localizable.strings new file mode 100644 index 000000000..ebbbe96e1 --- /dev/null +++ b/ownCloud/Resources/ar.lproj/Localizable.strings @@ -0,0 +1,495 @@ +/* + Localizable.strings + ownCloud + + Created by Pablo Carrascal on 13/03/2018. + Copyright © 2018 ownCloud GmbH. All rights reserved. +*/ + +/* + * Copyright (C) 2018, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +/* Add / Edit Bookmark */ +"Edit account" = "تعديل الحساب"; +"Add account" = "إضافة حساب"; +"Server URL" = "عنوان URL الخادم"; +"https://example.com" = "https://example.com"; +"Continue" = "متابعة"; +"Name" = "الاسم"; +"Example Server" = "خادم المثال"; +"Show Certificate Details" = "عرض تفاصيل الشهادة"; +"Connect" = "اتصال"; +"Delete Authentication Data" = "حذف بيانات المصادقة"; +"Authentication" = "مصادقة"; +"Username" = "اسم المستخدم"; +"Password" = "كلمة السر"; +"Certificate Details" = "تفاصيل الشهادة"; +"Cancel" = "إلغاء"; +"Approve" = "موافقة"; +"Error" = "خطأ"; +"Review Connection" = "مراجعة الاتصال"; +"Authenticated via" = "تمت المصادقة عبر"; +"Authenticated as %@ via %@" = "تمت مصادقة %@ عبر %@"; +"Edit" = "تعديل"; +"Credentials" = "بيانات الاعتماد"; +"Rejected" = "مرفوض"; +"Passed" = "تم الاجتياز"; +"Accepted" = "مقبول"; +"Validation Error" = "خطأ في التحقق"; +"Certificate was rejected by user." = "لقد رفض المستخدم الشهادة."; +"Certificate has issues.\nOpen 'Certificate Details' for more informations." = "هناك مشكلة بالشهادة.\nافتح 'تفاصيل الشهادة' لمزيد من المعلومات."; +"No issues found. Certificate passed validation." = "لم يتم العثور على أي مشكلات. اجتازت الشهادة عملية التحقق."; +"Certificate may have issues, but was accepted by user.\nOpen 'Certificate Details' for more informations." = "قد تكون هناك مشكلة بالشهادة، ولكن وافق عليها المستخدم.\nافتح 'تفاصيل الشهادة' لمزيد من المعلومات."; +"If you 'Continue', you will be prompted to allow the '%@' App to open OAuth2 login where you can enter your credentials." = "إذا قررت 'الاستمرار'، سيُطلب منك السماح لتطبيق '%@' بفتح ملف تسجيل الدخول إلى OAuth2 حيث يُمكنك إدخال بيانات اعتمادك."; + +"Contacting server…" = "جارٍ الاتصال بالخادم…"; +"Authenticating…" = "جارٍ المصادقة…"; + +"Fetching user information…" = "جارٍ إحضار معلومات المستخدم…"; +"Updating connection…" = "جارٍ تحديث الاتصال…"; + +"Missing hostname" = "اسم المضيف مفقود"; +"The entered URL does not include a hostname." = "لا يتضمن عنوان URL المُدخل اسم المضيف."; +"Add account" = "إضافة حساب"; +"Server name" = "اسم الخادم"; +"Server Password" = "كلمة مرور الخادم"; +"Server Username" = "اسم مستخدم الخادم"; + +/* Client */ +"Browse" = "تصفح"; +"Disconnect" = "قطع الاتصال"; +"Connecting…" = "جارٍ الاتصال…"; +"Connected." = "متصل."; +"Select" = "اختيار"; +"Done" = "تم"; + +"Folder" = "مجلد"; + +"Stopped" = "تم الإيقاف"; +"Started…" = "تم البدء…"; +"Contents from cache." = "محتويات من ذاكرة التخزين المؤقتة."; +"Waiting for server response…" = "انتظار استجابة الخادم…"; +"This folder no longer exists." = "لم يعد هذا المجلد مفقودًا."; +"Everything up-to-date." = "كل الأشياء محدثة."; +"Please wait…" = "يُرجى الانتظار…"; +"Sort by %@" = "ترتيب حسب %@"; +"Sort by" = "ترتيب حسب"; + +"name" = "الاسم"; +"type" = "النوع"; +"size" = "الحجم"; +"date" = "التاريخ"; +"Search this folder" = "بحث في هذا المجلد"; +"Pending" = "معلق"; +"Show parent paths" = "عرض المسارات الأصلية"; + +"%@ of %@ used" = "%@ الخاصة %@ المستخدمة"; +"Total: %@" = "إجمالي: %@"; + +/* Client Messages */ +"Empty folder" = "مجلد فارغ"; +"This folder contains no files or folders." = "لا يحتوي هذا المجلد على ملفات أو مجلدات."; + +"Folder removed" = "المجلد محذوف"; +"This folder no longer exists on the server." = "لم يعد هذا المجلد موجود في الخادم."; +"Are you sure you want to delete this item from the server?" = "هل تريد بالتأكيد حذف هذا العنصر من الخادم؟"; +"Are you sure you want to delete these items from the server?" = "هل تريد بالتأكيد حذف هذه العناصر من الخادم؟"; +"Multiple items" = "عناصر متعددة"; +"Deleting '%@'" = "حذف'%@'"; +"Renaming to %@" = "إعادة تسمية لـ %@"; +"Upload from your photo library" = "تحميل من مكتبة الصور"; +"Missing permissions" = "الأذونات مفقودة"; +"This permission is needed to upload photos and videos from your photo library." = "هذا الإذن مطلوب لتحميل الصور والفيديوهات من مكتبة الصور."; +"Not now" = "ليس الآن"; +"Upload file" = "تحميل ملف"; +"Upload files" = "تحميل ملفات"; + +"No matches" = "لا يوجد تطابقات"; +"There are no results for this search term" = "لا توجد نتائج عن البحث بهذا المصطلح"; +"Status" = "الحالة"; + +"Authorization failed" = "تعذر إجراء المصادقة"; +"The account has been disabled." = "تم تعطيل الحساب."; +"The server declined access with the credentials stored for this connection." = "رفض الخادم الوصول باستخدام بيانات الاعتماد المخزنة لهذا الاتصال."; +"Access denied" = "الوصول محظور"; +"The connection's access token has expired or become invalid. Sign in again to re-gain access." = "انتهت صلاحية الوصول للاتصال أو أصبح الوصول غير صالح. أعد تسجيل الدخول للحصول على وصول."; +"No authentication data has been found for this connection." = "لم يتم العثور على بيانات مصادقة لهذا الاتصال."; +"Sign in" = "تسجيل الدخول"; +"Ignore" = "تجاهل"; +"Continue offline" = "الاستمرار دون اتصال بالإنترنت"; +"Media upload in the previous session was incomplete since the application was terminated" = "Media upload in the previous session was incomplete since the application was terminated"; + +/* Server List*/ +"Cancel" = "إلغاء"; +"OK" = "تم"; + +"'%@' is currently locked" = "'%@' مقفول حاليًا"; +"An operation is currently performed that prevents connecting to '%@'. Please try again later." = "هناك عملية تُجرى حاليًا تحول دون الاتصال بـ '%@'. يُرجى المحاولة مجددًا."; +"Really delete '%@'?" = "هل تريد الحذف بالتأكيد '%@'؟"; +"This will also delete all locally stored file copies." = "سيحذف هذا جميع نسخ الملف المخزنة محليًا."; +"Delete" = "حذف"; +"Deletion of '%@' failed" = "تعذر حذف '%@'"; +"Accounts" = "حسابات"; + +"Help" = "المساعدة"; +"Feedback" = "ملاحظات"; +"Welcome" = "مرحباً"; +"Thanks for choosing %@! \n Start by adding your account." = "شكرًا على اختيارك %@! \n ابدأ بإضافة حسابك."; + +/* Settings Messages */ + +"Security" = "الأمان"; +"Passcode Lock" = "قفل رمز المرور"; +"Face ID" = "معرّف الوجه"; +"Touch ID" = "معرّف اللمسة"; +"Videos" = "فيديوهات"; +"Photos" = "صور"; +"Background uploads" = "تحميلات الخلفية"; +"Wifi only" = "Wifi فقط"; +"Settings" = "الإعدادات"; +"Lock application" = "تطبيق القفل"; +"Upload videos via WiFi only" = "تحميل فيديو عبر WiFi فقط"; +"Upload pictures via WiFi only" = "تحميل الصور عبر WiFi فقط"; +"More" = "المزيد"; +"Send feedback" = "إرسال التعليقات"; +"Recommend to a friend" = "توصية إلى صديق"; +"Privacy Policy" = "سياسة الخصوصية"; +"Acknowledgements" = "رسائل الإقرار"; +"Video upload path" = "مسار تحميل الفيديو"; +"Photo upload path" = "مسار تحميل الصورة"; +"Immediately" = "على الفور"; +"After 1 minute" = "بعد دقيقة واحدة"; +"After 5 minutes" = "بعد 5 دقائق"; +"After 30 minutes" = "بعد 30 دقيقة"; +"If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.\n\nAccess in Files.app is not possible, if you choose lock interval \"Immediately\".\nPlease choose an other delay, if you want to access your files in the Files.app or via a document picker." = "إذا اخترت \"Immediately\"، سيتم غلق التطبيق، فهو لم يعد موجودًا في المقدمة. الدخول إلى تطبيق \n\nAccess in Files غير متاح، إذا ما اخترت فترة الإقفال \"Immediately\".يُرجى \n اختيار مدة تأخير أخرى، إذا أردت الوصول إلى الملفات في تطبيق \"الملفات\" أو بواسطة منتقي المستندات."; +"Please configure an email account" = "يُرجى تكوين حساب البريد الإلكتروني"; +"You need to configure an email account first to be able to send emails." = "تحتاج إلى تكوين حساب البريد الإلكتروني أولًا حتى تتمكن من إرسال رسائل البريد الإلكتروني."; +"Do you want to open the following URL?" = "هل ترغب في فتح عنوان URL التالي؟"; + +"%@ %@ version %@ build %@ (app: %@, sdk: %@)" = "%@ %@ إصدار %@ إنشاء %@ (تطبيق: %@, sdk: %@)"; +"beta" = "بيتا"; +"release" = "إطلاق"; + + +/* User Interface Settings */ +"Theme" = "لون الخلفية"; +"User Interface" = "واجهة المستخدم"; +"Dark" = "داكن"; +"Light" = "ساطع"; +"Classic" = "كلاسيكي"; +"System" = "النظام"; +"System Appeareance" = "System Appeareance"; + +/* Log settings */ +"Log Files" = "ملفات السجل"; +"Browse log files" = "ملفات سجل التصفح"; +"Share" = "مشاركة"; +"Delete all log files?" = "هل تريد حذف جميع ملفات السجل؟"; +"Delete all" = "حذف الكل"; + +"Log Level" = "مستوى السجل"; +"Log Destinations" = "وجهات السجل"; +"Privacy" = "خصوصية"; + +"Logging" = "جارِ التسجيل"; +"Enable logging" = "تمكين التسجيل"; +"Options" = "خيارات"; + +"Off" = "إيقاف التشغيل"; +"Info" = "معلومات"; +"Default" = "افتراضي"; +"Warning" = "تحذير"; +"Error" = "خطأ"; + +"Share log file" = "ملف سجل المشاركة"; +"Reset log file" = "إعادة تعيين ملف السجل"; + +"When activated, logs may impact performance and include sensitive information. However the logs are not subject to automatic submission to %@ servers. Sharing logs with others is sole user responsibility." = "عند التنشيط، قد تؤثر السجلات في الأداء وقد تشمل محتويات حساسة. ومع هذا، لا تخضع السجلات إلى الإرسال الآلي لخوادم %@ . إنَّ مشاركة السجلات مع مستخدمين آخرين هي مسؤولية المستخدم وحده."; + +"The last 10 archived logs are kept on the device - with each log covering up to 24 hours of usage. When sharing please bear in mind that logs may contain sensitive information such as server URLs and user-specific information." = "السجلات العشرة الأخيرة محفوظة على الجهاز - يحمل كل سجل تغطية ذاكرة تصل إلى 24 ساعة من الاستخدام. عند المشاركة، يُرجى الأخذ في الاعتبار أن السجلات قد تحتوي على معلومات حساسة مثل عناوين URL للخادم ومعلومات خاصة بالمستخدم."; + +"Mask private data" = "إخفاء البيانات الخاصة"; +"Enabling this option will attempt to mask private data, so it does not become part of any log. Since logging is a development and debugging feature, though, we can't guarantee that the log file will be free of any private data even with this option enabled. Therefore, please look through any log file and verify its free of any data you're not comfortable sharing before sharing it with anybody." = "سيعمل تمكين هذا الخيار على إخفاء البيانات الخاصة، كي لا تصبح جزءًا من أي سجل. نظرًا لأن التسجيل ميزة تطويرية ومتعلقة بالتصحيح، ورغم هذا، لا يمكن ضمان خلو ملف السجل من أي بيانات خاصة حتى بعد تمكين هذا الخيار. ولهذا، يُرجى فحص أي ملف سجل والتحقق من خلوه من أي بيانات لا ترغب في مشاركتها قبل مشاركة هذا مع أي شخص."; + +"No log file found" = "لم يتم العثور على أي ملف سجل"; +"The log file can't be shared because no log file could be found or the log file is empty." = "تعذر مشاركة ملف السجل لأنه لم يتم العثور على أي ملف سجل أو أن ملف السجل فارغ."; +"Enable log file" = "تمكين ملف السجل"; + +"Really reset log file?" = "هل تريد إعادة تعيين ملف السجل؟"; +"This action can't be undone." = "لا يمكن التراجع عن هذا الإجراء."; + +/* Storage settings */ +"Delete unused local copies" = "حذف نسخ محلية غير مستخدمة"; +"Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "الوقت المقدّر منذ تحميل الملف المعني أو تعديله أو تنزيله أو عرضه بواسطة هذا الجهاز. لا ينطبق على أي من الملفات المحملة بواسطة خاصية التوافر مع عدم الاتصال بالإنترنت. يمكن حذف النسخ المحلية قبل انتهاء المدة المحددة، على سبيل المثال، بعد أن تتوفر نسخة أحدث من الملف على الخادم - أو من خلال الحذف اليدوي للنسخ غير المتصلة بالإنترنت. وأيضًا، لا يمكن حذف النسخ المحلية قبل انتهاء المدة المحددة، على سبيل المثال، إذا تم تنفيذ إجراء ما عليها، أو أن الملف قيد الاستخدام - أو لم يتم بعد استخدام الحساب الذي يحتوي على الملف في التطبيق."; +"never" = "أبدًا"; +"after %@" = "بعد %@"; +"Decrease Slider Value" = "Decrease Slider Value"; +"Increase Slider Value" = "Increase Slider Value"; + +/* Display settings */ +"Display settings" = "عرض الإعدادات"; +"Show hidden files and folders" = "عرض الملفات والمجلدات المختفية"; + +/* Passcode Messages */ + +"Enter code" = "إدخال الرمز"; +"Repeat code" = "تكرار الرمز"; +"Delete code" = "حذف الرمز"; +"The entered codes are different" = "الرموز المدخلة مختلفة"; +"Incorrect code" = "رمز غير صحيح"; +"Please try again in %@" = "يُرجى المحاولة مجددًا %@"; +"Unlock %@" = "إلغاء القفل %@"; +"Biometric authentication failed" = "تعذر إجراء المصادقة البيولوجية"; + +/* Certificate management */ + +"Certificates" = "شهادات"; +"User-approved certificates" = "شهادات المستخدم المعتمدة"; + +"Approved" = "معتمدة"; +"Auto-approved" = "معتمدة آليًا"; +"Revoke approval" = "Revoke approval"; + +/* Actions */ + +"Forbidden Characters" = "أحرف ممنوعة"; +"File name cannot contain / or \\" = "لا يمكن أن يحتوي اسم الملف على / أو \\"; +"New Folder" = "مجلد جديد"; +"Folder name" = "اسم المجلد"; +"Rename" ="Rename"; +"Create folder" ="انشئ مجلد"; +"Duplicate" = "تكرار"; +"Move" = "نقل"; +"Open in" = "فتح في"; +"Copy" = "نسخ"; +"Copy here" = "نسخ هنا"; +"Cannot connect to " = "تعذر الاتصال بـ "; +" couldn't download file(s)" = " تعذر تنزيل الملف (الملفات)"; +"Actions" = "إجراءات"; +"copy" = "نسخ"; +"Close Window" = "Close Window"; +"Open in a new Window" = "Open in a new Window"; +"Open in Window" = "Open in Window"; + +"Preparing…" = "جارٍ الإعداد…"; + +/* Directory Picker Messages */ +"Move here" = "نقل هنا"; + +/* Preview */ +"Open file" = "فتح الملف"; +"Network unavailable" = "الشبكة غير متوفرة"; +"Error" = "خطأ"; +"Could not get the picture" = "تعذر الحصول على الصورة"; +"Downloading" = "جارٍ التنزيل"; +"File couldn't be opened" = "تعذر فتح الملفات"; + +/* PDF Viewer */ +"Resume" = "استئناف"; +"Go to page" = "ذهاب إلى الصفحة"; +"Page" = "صفحة"; +"This document has %@ pages" = "يحتوي هذا المستند على عدد %@ صفحة"; +"%@ of %@" = "%@ من %@"; +"Invalid Page" = "صفحة غير صالحة"; +"The entered page number doesn't exist" = "رقم الصفحة المكتوب غير موجود"; +"Search PDF" = "بحث في ملف PDF"; +"Outline" = "مخطط تفصيلي"; + +/* Photo Upload */ +"Upload" = "تحميل"; +"Select All" = "تحديد الكل"; +"Deselect All" = "إلغاء تحديد الكل"; +"All Photos" = "جميع الصور"; +"Albums" = "الألبومات"; +"Importing from photo library" = "استيراد من مكتبة صورك"; + +/* Scan */ +"Scan" = "Scan"; +"Scans" = "Scans"; +"Scan document" = "Scan document"; +"Saving" = "Saving"; +"File format" = "File format"; +"Name" = "الاسم"; +"Save as" = "Save as"; +"Options" = "خيارات"; +"Scan additional" = "Scan additional"; +"Create one file per page" = "Create one file per page"; + +/* Sharing */ +"Searching Shares…" = "جارِ البحث في المشاركات…"; +"Recipient" = "مستلم"; +"Recipients" = "مستلمون"; +"Public Link" = "ارتباط عام"; +"Public Links" = "ارتباطات عامة"; +"Shared by %@" = "مشاركة بواسطة %@"; +"Invite Recipient" = "إرسال دعوة إلى مستلم"; +"Recipients" = "مستلمون"; +"Add email or name" = "إضافة بريد إلكتروني أو اسم"; +"Users" = "مستخدمون"; +"Groups" = "مجموعات"; +"Start typing to search users, groups and remote users." = "ابدأ الكتابة للبحث عن المستخدمين والمجموعات والمستخدمين عن بُعد."; +"(Group)" = "(مجموعة)"; +"Adding User to Share failed" = "تعذر إضافة مستخدم إلى المشاركة"; +"Permissions" = "أذونات"; +"Invited: %@" = "تمت الدعوة: %@"; +"Created: %@" = "تم الإنشاء: %@"; +"Allows the users you share with to re-share" = "يُتيح للمستخدمين الذين تشاركهم إعادة المشاركة"; +"Allows the users you share with to edit your shared files, and to collaborate" = "يُتيح للمستخدمين الذين تشاركهم تعديل الملفات التي تم مشاركتها والتعاون فيها"; +"Allows the users you share with to create new files and add them to the share" = "يُتيح للمستخدمين الذين تشاركهم إنشاء ملفات جديدة وإضافتهم للمشاركة"; +"Allows uploading a new version of a shared file and replacing it" = "يُتيح تحميل إصدار جديد من الملفات التي تم مشاركتها واستبدالها"; +"Allows the users you share with to delete shared files" = "يُتيح للمستخدمين الذين تشاركهم حذف الملفات التي تشاركها"; +"Setting permission failed" = "تعذر إعداد الإذن"; +"Shared with" = "تمت المشاركة مع"; +"Remove Recipient failed" = "تعذر إزالة المستلم"; +"Remove Recipient" = "إزالة المستلم"; +"Create" = "إنشاء"; +"Change" = "تغيير"; +"Recipients can view or download contents." = "يستطيع المستلمون عرض المحتويات أو تنزيلها."; +"Recipients can view, download, edit, delete and upload contents." = "يستطيع المستلمون عرض المحتويات أو تنزيلها أو تعديلها أو حذفها أو تحميلها."; +"Receive files from multiple recipients without revealing the contents of the folder." = "استلام ملفات من مستلمين متعددين دون إظهار محتويات المجلد."; +"Download / View" = "تنزيل / عرض"; +"Download / View / Upload" = "تنزيل / عرض/ تحميل"; +"Upload only (File Drop)" = "تحميل الملف فقط (إفلات الملف)"; +"Creating public link failed" = "تعذر إنشاء ارتباط عام"; +"Create Public Link" = "إنشاء ارتباط عام"; +"Links" = "ارتباطات"; +"Link" = "ارتباط"; +"Setting expiration date failed" = "تعذر إعداد تاريخ انتهاء الصلاحية"; +"Expiration date" = "تاريخ إنتهاء الصلاحية"; +"Copy Public Link" = "نسخ ارتباط عام"; +"Delete Public Link" = "حذف ارتباط عام"; +"Deleting Public Link failed" = "تعذر حذف ارتباط عام"; +"Deleting password failed" = "تعذر حذف كلمة المرور"; +"Setting password failed" = "تعذر تعيين كلمة المرور"; +"Type to update password" = "اكتب لتحديث كلمة المرور"; +"Cannot change permission" = "تعذر تغيير الإذن"; +"Before you can set the permission\n%@,\n you must enter a password." = "قبل تعيين الإذن\n%@,\n يجب إدخال كلمة المرور."; +"Password Protected" = "كلمة المرور محمية"; +"Pending Federated Invites" = "تعليق الدعوات الموحدة"; +"Pending Invites" = "تعليق الدعوات"; +"Shared with you" = "تمت المشاركة معك"; +"Shared with others" = "تمت المشاركة مع الآخرين"; +"Shares" = "المشاركات"; +"Copy Private Link" = "نسخ ارتباط خاص"; +"Only recipients can use this link. Use it as a permanent link to point to this resource" = "يستطيع المستلمون فقط استخدام هذا الارتباط. استخدم الارتباط كارتباط دائم للإشارة إلى هذا المصدر"; +"Accept Share failed" = "تعذر الموافقة على المشاركة"; +"Decline Share failed" = "تعذر رفض المشاركة"; +"Accept" = "قبول"; +"Decline" = "رفض"; +"Declined" = "رفض"; +"Decline Share" = "رفض المشاركة"; +"Unshare" = "إلغاء المشاركة"; +"Unshare failed" = "تعذر إلغاء المشاركة"; +"Are you sure you want to unshare these items?" = "هل تريد بالتأكيد إلغاء مشاركة هذه العناصر؟"; +"Are you sure you want to unshare this item?" = "هل تريد بالتأكيد إلغاء مشاركة هذا العنصر؟"; +"Share" = "مشاركة"; +"Read" = "قراءة"; +"Can Share" = "يمكن المشاركة"; +"Can Edit" = "يمكن التعديل"; +"Can Edit and Change" = "يمكن التعديل والتغيير"; +"Can Create" = "يمكن الإنشاء"; +"Can Change" = "يمكن التغيير"; +"Can Delete" = "يمكن الحذف"; +"Accept Invite %@" = "قبول الدعوة %@"; +"Decline Invite %@" = "رفض الدعوة %@"; +"Decline cannot be undone." = "تعذر التراجع عن الرفض."; +"Sharing" = "مشاركة"; +"You" = "أنت"; +"Share this file" = "مشاركة هذا الملف"; +"Share this folder" = "مشاركة هذا المجلد"; +"shared" = "مُشارك"; +"Owner" = "المالك"; +"Private Link" = "ارتباط خاص"; + +/* Quick Access view */ +"Quick Access" = "وصول سريع"; +"Collection" = "مجموعة"; +"Recents" = "الأخيرة"; +"Favorites"= "المفضلة"; +"Images" = "صور"; +"PDF Documents" = "مستندات PDF"; + +/* Media files settings */ +"Media Files" = "ملفات الوسائط"; +"Streaming Enabled" = "تم تمكين البث"; + +/* Photo upload settings */ +"Media Upload" = "تحميل الوسائط"; +"Convert HEIC to JPEG" = "تحويل HEIC إلى JPEG"; +"Convert videos to MP4" = "تحويل الفيديوهات إلى امتداد MP4"; +"Auto Upload Photos" = "Instant Upload Photos"; +"Auto Upload Videos" = "Instant Upload Videos"; +"Account" = "حساب"; +"Accounts" = "حسابات"; +"Select account" = "Select account"; +"Upload Path" = "Upload Path"; +"Select Upload Path" = "Select Upload Path"; +"Auto upload of media was disabled since configured account / folder was not found" = "Auto upload of media was disabled since configured account / folder was not found"; + +/* Progress summarizer */ +"Creating %ld folders…" = "إنشاء مجلدات %ld…"; +"Moving %ld items…" = "نقل عناصر %ld…"; +"Copying %ld items…" = "نسخ عناصر %ld…"; +"Deleting %ld items…" = "حذف عناصر %ld…"; +"Uploading %ld files…" = "تحميل ملفات %ld…"; +"Downloading %ld files…" = "تنزيل ملفات %ld…"; +"Updating %ld items…" = "تحميل عناصر %ld…"; + +/* Offline storage management */ +"Free on %@" = "مجاني في %@"; +"unknown" = "غير معروف"; +"Offline files use" = "استخدام الملفات دون اتصال"; +"Compacting of '%@' failed" = "تعذر ضغط '%@'"; +"Include available offline files" = "تضمين ملفات متوفرة دون اتصال"; +"Delete Local Copies" = "حذف نسخ محلية"; +"Manage" = "إدارة"; +"Storage" = "وحدة التخزين"; +"Compacting" = "Compacting"; + +"Really include available offline files?" = "هل تريد تضمين ملفات متوفرة دون اتصال؟"; +"Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "ستصبح الملفات والمجلدات المُشار إليها بـ(متوفرة دون اتصال) غير متوفرة. سيتم إعادة تحميلها في المرة القادمة التي تسجل فيها الدخول إلى حسابك (الاتصال مطلوب)."; + +/* Available offline */ +"Root folder" = "المجلد الجذر"; +"at" = "في"; +"(no match)" = "(لا يوجد تطابق)"; + +"Make available offline" = "اجعل العنصر متوفر دون اتصال"; +"Make unavailable offline" = "اجعل العنصر غير متوفر دون اتصال"; + +"Available Offline" = "متوفر دون اتصال"; +"No items have been selected for offline availability." = "لم يتم اختيار أي عنصر للتوفر دون اتصال."; + +"Overview" = "نظرة عامة"; +"All Files" = "جميع الملفات"; + +/* Import File */ +"Save File" = "حفظ الملفات"; +"Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "اختر حسابًا ومجلدًا لاستيراد الملف إليه.\n\nيمكن استيراد ملف واحد في المرة الواحدة."; + +/* Key Commands */ +"Select Next" = "Select Next"; +"Select Previous" = "Select Previous"; +"Open Selected" = "Open Selected"; +"Change Sort Order" = "Change Sort Order"; +"Search" = "بحث"; +"Back" = "رجوع إلى الخلف"; +"Save" = "حفظ"; +"Sort by %@" = "ترتيب حسب %@"; +"Tab %@" = "Tab %@"; +"Select Last Item on Page" = "Select Last Item on Page"; +"Scroll to Top" = "Scroll to Top"; +"Scroll to Bottom" = "Scroll to Bottom"; +"Copy to Pasteboard" = "Copy to Pasteboard"; +"Import from Pasteboard" = "Import from Pasteboard"; +"Next" = "التالي"; +"Previous" = "السابق"; +"Favorite" = "المفضلة"; +"Cut" = "Cut"; diff --git a/ownCloud/Resources/cs.lproj/Localizable.strings b/ownCloud/Resources/cs.lproj/Localizable.strings index c0996690d..9da3f9e60 100644 --- a/ownCloud/Resources/cs.lproj/Localizable.strings +++ b/ownCloud/Resources/cs.lproj/Localizable.strings @@ -98,7 +98,6 @@ "Passcode Lock" = "Přístupový kód"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Okamžitá odesílání"; "Videos" = "Videa"; "Photos" = "Fotky"; "Background uploads" = "Nahrávání na pozadí"; diff --git a/ownCloud/Resources/de-DE.lproj/Localizable.strings b/ownCloud/Resources/de-DE.lproj/Localizable.strings index 5f4329f1f..7b377aeb4 100644 --- a/ownCloud/Resources/de-DE.lproj/Localizable.strings +++ b/ownCloud/Resources/de-DE.lproj/Localizable.strings @@ -124,6 +124,7 @@ "Sign in" = "Einloggen"; "Ignore" = "Ignorieren"; "Continue offline" = "Offline fortfahren"; +"Media upload in the previous session was incomplete since the application was terminated" = "Der Medien-Upload in der vorherigen Sitzung war unvollständig, da die Anwendung beendet wurde."; /* Server List*/ "Cancel" = "Abbrechen"; @@ -148,7 +149,6 @@ "Passcode Lock" = "PIN-Sperre"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Sofortiges Hochladen"; "Videos" = "Videos"; "Photos" = "Fotos"; "Background uploads" = "Hochladen im Hintergrund"; @@ -160,7 +160,7 @@ "More" = "Mehr"; "Send feedback" = "Feedback senden"; "Recommend to a friend" = "Dies einem Freund empfehlen"; -"Privacy Policy" = "Datenschutzerkärung"; +"Privacy Policy" = "Datenschutzerklärung"; "Acknowledgements" = "Danksagungen"; "Video upload path" = "Pfad für den Video-Upload"; "Photo upload path" = "Upload Pfad für Fotos"; @@ -184,6 +184,8 @@ "Dark" = "Dunkel"; "Light" = "Hell"; "Classic" = "Klassik"; +"System" = "System"; +"System Appeareance" = "System Erscheinungsbild"; /* Log settings */ "Log Files" = "Protokolldateien"; @@ -228,6 +230,8 @@ "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Gemessene Zeit seit dem Hochladen, Bearbeiten, Herunterladen oder Anzeigen der jeweiligen Datei über dieses Gerät. Gilt nicht für Dateien, die über die Funktion Offline verfügbar heruntergeladen wurden. Lokale Kopien können vor Ablauf des angegebenen Zeitraums gelöscht werden, z.B. weil es eine neuere Version einer Datei auf dem Server gibt - oder durch das manuelle Löschen von Offline-Kopien. Außerdem dürfen lokale Kopien nach Ablauf der angegebenen Zeit nicht gelöscht werden, z.B. wenn eine Aktion an ihr durchgeführt wird, die Datei noch in Gebrauch ist - oder das Konto, das die Datei enthält, nicht in der App verwendet wurde."; "never" = "Niemals"; "after %@" = "nach %@"; +"Decrease Slider Value" = "Decrease Slider Value"; +"Increase Slider Value" = "Increase Slider Value"; /* Display settings */ "Display settings" = "Anzeigeeinstellungen"; @@ -270,6 +274,9 @@ " couldn't download file(s)" = "Datei(en) konnte nicht geladen werden"; "Actions" = "Aktionen"; "copy" = "Kopie"; +"Close Window" = "Close Window"; +"Open in a new Window" = "Open in a new Window"; +"Open in Window" = "Open in Window"; "Preparing…" = "Vorbereiten..."; @@ -303,6 +310,18 @@ "Albums" = "Alben"; "Importing from photo library" = "Importieren aus der Fotobibliothek"; +/* Scan */ +"Scan" = "Scan"; +"Scans" = "Scans"; +"Scan document" = "Scan document"; +"Saving" = "Speichern"; +"File format" = "File format"; +"Name" = "Name"; +"Save as" = "Speichern unter"; +"Options" = "Optionen"; +"Scan additional" = "Scan additional"; +"Create one file per page" = "Create one file per page"; + /* Sharing */ "Searching Shares…" = "Suche nach Freigaben…"; "Recipient" = "Empfänger"; @@ -405,6 +424,14 @@ "Media Upload" = "Medienupload"; "Convert HEIC to JPEG" = "Konvertiere HEIC zu JPEG"; "Convert videos to MP4" = "Videos konvertieren zu MP4"; +"Auto Upload Photos" = "Sofortiges hochladen der Bilder "; +"Auto Upload Videos" = "Sofortiges hochladen der Videos"; +"Account" = "Benutzerkonto"; +"Accounts" = "Konten"; +"Select account" = "Account auswählen"; +"Upload Path" = "Upload Pfad"; +"Select Upload Path" = "Upload-Pfad auswählen"; +"Auto upload of media was disabled since configured account / folder was not found" = "Auto Upload von Medien wurde deaktiviert, da das konfigurierte Konto / Ordner nicht gefunden wurde."; /* Progress summarizer */ "Creating %ld folders…" = "Erstelle %ld Ordner…"; @@ -424,11 +451,10 @@ "Delete Local Copies" = "Lösche lokale Kopien"; "Manage" = "Verwalten"; "Storage" = "Speicher"; -"Proceed" = "Fortfahren"; "Compacting" = "Freigeben"; "Really include available offline files?" = "Offline-Dateien wirklich einbeziehen?"; -"Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "Dateien und Ordner, die als Offline Verfügbar markiert sind, sind nicht mehr vorhanden. Sie werden beim nächsten Login in Ihr Konto erneut heruntergeladen (Konnektivität erforderlich)."; +"Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "Dateien und Ordner, die als Offline Verfügbar markiert sind, werden nicht mehr verfübar sein. Sie werden beim nächsten Login in Ihr Konto erneut heruntergeladen (Konnektivität erforderlich)."; /* Available offline */ "Root folder" = "Hauptverzeichnis"; @@ -443,8 +469,27 @@ "Overview" = "Übersicht"; "All Files" = "Alle Dateien"; -"Locations" = "Speicherorte"; /* Import File */ "Save File" = "Datei speichern"; "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Wählen Sie ein Konto und einen Ordner, in den die Datei importiert werden soll.\n\nEs kann nur eine Datei auf einmal importiert werden."; + +/* Key Commands */ +"Select Next" = "Select Next"; +"Select Previous" = "Select Previous"; +"Open Selected" = "Open Selected"; +"Change Sort Order" = "Change Sort Order"; +"Search" = "Suche"; +"Back" = "Zurück"; +"Save" = "Speichern"; +"Sort by %@" = "Sortiert nach %@"; +"Tab %@" = "Tab %@"; +"Select Last Item on Page" = "Select Last Item on Page"; +"Scroll to Top" = "Scroll to Top"; +"Scroll to Bottom" = "Scroll to Bottom"; +"Copy to Pasteboard" = "Copy to Pasteboard"; +"Import from Pasteboard" = "Import from Pasteboard"; +"Next" = "Weiter"; +"Previous" = "Zurück"; +"Favorite" = "Favorit"; +"Cut" = "Cut"; diff --git a/ownCloud/Resources/de.lproj/Localizable.strings b/ownCloud/Resources/de.lproj/Localizable.strings index db5c2bace..7b377aeb4 100644 --- a/ownCloud/Resources/de.lproj/Localizable.strings +++ b/ownCloud/Resources/de.lproj/Localizable.strings @@ -19,24 +19,24 @@ /* Add / Edit Bookmark */ "Edit account" = "Konto bearbeiten"; "Add account" = "Konto hinzufügen"; -"Server URL" = "Server-URL"; -"https://example.com" = "https://beispiel.com"; +"Server URL" = "Serveradresse"; +"https://example.com" = "https://example.com"; "Continue" = "Fortsetzen"; "Name" = "Name"; -"Example Server" = "Beispielserver"; -"Show Certificate Details" = "Zeige Zertifikatsdetails"; +"Example Server" = "Beispiel Server"; +"Show Certificate Details" = "Zertifikats-Details anzeigen"; "Connect" = "Verbinden"; -"Delete Authentication Data" = "Lösche Anmeldedaten"; +"Delete Authentication Data" = "Authentifizierungs-Daten löschen"; "Authentication" = "Authentifizierung"; "Username" = "Benutzername"; -"Password" = "Kennwort"; -"Certificate Details" = "Zertifikatsdetails"; +"Password" = "Passwort"; +"Certificate Details" = "Zertifikats-Details"; "Cancel" = "Abbrechen"; "Approve" = "Genehmigen"; "Error" = "Fehler"; -"Review Connection" = "Verbindung überprüfen"; -"Authenticated via" = "Angemeldet mit"; -"Authenticated as %@ via %@" = "Als %@ mit %@ angemeldet"; +"Review Connection" = "Verbindung prüfen"; +"Authenticated via" = "Authentifiziert über "; +"Authenticated as %@ via %@" = "Authentifiziert als %@ über %@"; "Edit" = "Bearbeiten"; "Credentials" = "Zugangsdaten"; "Rejected" = "Abgelehnt"; @@ -49,14 +49,14 @@ "Certificate may have issues, but was accepted by user.\nOpen 'Certificate Details' for more informations." = "Das Zertifikat hat möglicherweise Probleme, wurde aber vom Benutzer akzeptiert.\nÖffnen Sie 'Zertifikat-Details' für weitere Informationen."; "If you 'Continue', you will be prompted to allow the '%@' App to open OAuth2 login where you can enter your credentials." = "Wenn Sie auf 'Fortsetzen' klicken, werden Sie aufgefordert, der App '%@' zu erlauben, die Anmeldung bei OAuth2 zu öffnen, wo Sie Ihre Zugangsdaten eingeben können."; -"Contacting server…" = "Kontaktiere Server..."; -"Authenticating…" = "Authentifiziere"; +"Contacting server…" = "Verbinde zu Server ..."; +"Authenticating…" = "Anmeldung läuft"; "Fetching user information…" = "Nutzerinformationen abrufen ..."; "Updating connection…" = "Aktualisierung der Verbindung..."; "Missing hostname" = "Hostname fehlt"; -"The entered URL does not include a hostname." = "Eingegebene URL enthält keinen Hostnamen."; +"The entered URL does not include a hostname." = "Die eingegebene URL Adresse enthält keinen Hostname"; "Add account" = "Konto hinzufügen"; "Server name" = "Server Name"; "Server Password" = "Server Passwort"; @@ -65,28 +65,28 @@ /* Client */ "Browse" = "Durchsuchen"; "Disconnect" = "Trennen"; -"Connecting…" = "Verbinde..."; -"Connected." = "Verbunden."; +"Connecting…" = "Verbinde ..."; +"Connected." = "Verbunden"; "Select" = "Auswählen"; "Done" = "Erledigt"; "Folder" = "Ordner"; -"Stopped" = "Angehalten"; -"Started…" = "Gestartet..."; -"Contents from cache." = "Inhalte vom Cache."; -"Waiting for server response…" = "Warte auf Serverantwort..."; +"Stopped" = "Gestoppt"; +"Started…" = "Gestartet"; +"Contents from cache." = "Inhalte aus Zwischenspeicher."; +"Waiting for server response…" = "Warte auf Server-Antwort ..."; "This folder no longer exists." = "Dieser Ordner existiert nicht mehr."; -"Everything up-to-date." = "Alles aktuell."; -"Please wait…" = "Bitte warten..."; -"Sort by %@" = "Sortieren nach %@"; +"Everything up-to-date." = "Alles ist aktuell."; +"Please wait…" = "Bitte warten ..."; +"Sort by %@" = "Sortiert nach %@"; "Sort by" = "Sortieren nach"; "name" = "Name"; "type" = "Typ"; "size" = "Größe"; "date" = "Datum"; -"Search this folder" = "Diesen Ordner durchsuchen"; +"Search this folder" = "Ordner durchsuchen"; "Pending" = "Ausstehend"; "Show parent paths" = "Übergeordnete Pfade anzeigen"; @@ -95,18 +95,18 @@ /* Client Messages */ "Empty folder" = "Leerer Ordner"; -"This folder contains no files or folders." = "Dieser Ordner enthält keine Dateien oder Ordner."; +"This folder contains no files or folders." = "Dieser Ordner enthält keine Dateien oder andere Ordner"; -"Folder removed" = "Ordner entfernt"; -"This folder no longer exists on the server." = "Dieser Ordner existiert nicht mehr auf diesem Server."; -"Are you sure you want to delete this item from the server?" = "Sind Sie sich sicher dieses Element vom Server zu löschen?"; -"Are you sure you want to delete these items from the server?" = "Sind Sie sicher, dass sie diese Items vom Server löschen möchten?"; +"Folder removed" = "Ordner gelöscht"; +"This folder no longer exists on the server." = "Der Ordner existiert nicht mehr auf dem Server"; +"Are you sure you want to delete this item from the server?" = "Sind Sie sicher, dass Sie dieses Objekt vom Server löschen möchten?"; +"Are you sure you want to delete these items from the server?" = "Sind Sie sicher, dass Sie diese Objekte vom Server löschen möchten?"; "Multiple items" = "Mehrere Objekte"; -"Deleting '%@'" = "Lösche \"%@\""; -"Renaming to %@" = "Ändere Namen auf %@"; +"Deleting '%@'" = "Lösche '%@'"; +"Renaming to %@" = "Umbenennen in %@"; "Upload from your photo library" = "Hochladen aus Ihrer Fotobibliothek"; -"Missing permissions" = "Fehlende Berechtigung"; -"This permission is needed to upload photos and videos from your photo library." = "Diese Berechtigung wird benötigt, um Fotos und Videos aus Ihrer Fotobibliothek hochzuladen."; +"Missing permissions" = "Fehlende Berechtigungen"; +"This permission is needed to upload photos and videos from your photo library." = "Diese Berechtigung ist erforderlich, um Fotos und Videos aus deinem Fotoalbum hochzuladen."; "Not now" = "Nicht jetzt"; "Upload file" = "Datei hochladen"; "Upload files" = "Dateien hochladen"; @@ -124,17 +124,18 @@ "Sign in" = "Einloggen"; "Ignore" = "Ignorieren"; "Continue offline" = "Offline fortfahren"; +"Media upload in the previous session was incomplete since the application was terminated" = "Der Medien-Upload in der vorherigen Sitzung war unvollständig, da die Anwendung beendet wurde."; /* Server List*/ "Cancel" = "Abbrechen"; "OK" = "OK"; -"'%@' is currently locked" = "\"%@\" wird derzeit verwendet"; -"An operation is currently performed that prevents connecting to '%@'. Please try again later." = "Ein Vorgang wird derzeit ausgeführt, der die Verbindung zu \"%@\" verhindert. Bitte versuchen Sie es später nochmal."; +"'%@' is currently locked" = "%@ ist gerade gesperrt"; +"An operation is currently performed that prevents connecting to '%@'. Please try again later." = "Die Verbindung zu %@ wird gerade durch eine laufende Operation verhindert. Bitte später probieren."; "Really delete '%@'?" = "\"%@\" wirklich löschen?"; -"This will also delete all locally stored file copies." = "Das wird auch alle lokale Kopien vom Gerät entfernen."; +"This will also delete all locally stored file copies." = "Dies löscht auch alle lokalen Datei-Kopien"; "Delete" = "Löschen"; -"Deletion of '%@' failed" = "\"%@\" konnte nicht gelöscht werden"; +"Deletion of '%@' failed" = "Löschen von 1%@ ist fehlgeschlagen"; "Accounts" = "Konten"; "Help" = "Hilfe"; @@ -147,8 +148,7 @@ "Security" = "Sicherheit"; "Passcode Lock" = "PIN-Sperre"; "Face ID" = "Face ID"; -"Touch ID" = "Fingerabdruck-ID"; -"Instant Uploads" = "Sofortiges Hochladen"; +"Touch ID" = "Touch ID"; "Videos" = "Videos"; "Photos" = "Fotos"; "Background uploads" = "Hochladen im Hintergrund"; @@ -159,18 +159,18 @@ "Upload pictures via WiFi only" = "Fotos nur über WiFi hochladen"; "More" = "Mehr"; "Send feedback" = "Feedback senden"; -"Recommend to a friend" = "Empfehle dies einem Freund"; -"Privacy Policy" = "Datenschutzerkärung"; +"Recommend to a friend" = "Dies einem Freund empfehlen"; +"Privacy Policy" = "Datenschutzerklärung"; "Acknowledgements" = "Danksagungen"; -"Video upload path" = "Pfad für Video-Upload"; -"Photo upload path" = "Foto-Upload-Pfad"; +"Video upload path" = "Pfad für den Video-Upload"; +"Photo upload path" = "Upload Pfad für Fotos"; "Immediately" = "Sofort"; -"After 1 minute" = "Nach 1 Minute"; -"After 5 minutes" = "Nach 5 Minuten"; -"After 30 minutes" = "Nach 30 Minuten"; -"If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.\n\nAccess in Files.app is not possible, if you choose lock interval \"Immediately\".\nPlease choose an other delay, if you want to access your files in the Files.app or via a document picker." = "Wenn du \"Sofort\" wählst, wird die App gesperrt, wenn sie nicht mehr im Vordergrund ist.\n\nDer Zugriff in Files.app ist nicht möglich, wenn du das Sperrintervall \"Sofort\" wählst.\nBitte wähle eine andere Verzögerung, wenn du auf deine Dateien in der Files.app oder über eine Dokumentauswahl, zugreifen möchtest."; -"Please configure an email account" = "Bitte richten Sie ein E-Mail-Konto ein"; -"You need to configure an email account first to be able to send emails." = "Sie müssen zuerst ein E-Mail-Konto einrichten, um E-Mails senden zu können."; +"After 1 minute" = "nach 1 Minute"; +"After 5 minutes" = "nach 5 Minuten"; +"After 30 minutes" = "nach 30 Minuten"; +"If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.\n\nAccess in Files.app is not possible, if you choose lock interval \"Immediately\".\nPlease choose an other delay, if you want to access your files in the Files.app or via a document picker." = "Wenn Sie \"Sofort\" wählen, wird die App gesperrt, wenn sie nicht mehr im Vordergrund ist.\n\nDer Zugriff in Files.app ist nicht möglich, wenn Sie das Sperrintervall \"Sofort\" wählen.\nBitte wählen Sie eine andere Verzögerung, wenn Sie auf Ihre Dateien in der Files.app oder über eine Dokumentauswahl, zugreifen möchten."; +"Please configure an email account" = "Bitte ein E-Mail Konto konfigurieren"; +"You need to configure an email account first to be able to send emails." = "Sie müssen zuerst ein E-Mail Konto konfigurieren, um Mails zu versenden"; "Do you want to open the following URL?" = "Möchten Sie die folgende URL öffnen?"; "%@ %@ version %@ build %@ (app: %@, sdk: %@)" = "%@ %@ Version %@ Build %@ (App: %@, SDK: %@)"; @@ -184,6 +184,8 @@ "Dark" = "Dunkel"; "Light" = "Hell"; "Classic" = "Klassik"; +"System" = "System"; +"System Appeareance" = "System Erscheinungsbild"; /* Log settings */ "Log Files" = "Protokolldateien"; @@ -194,7 +196,7 @@ "Log Level" = "Protokollierungsstufe"; "Log Destinations" = "Protokollierungsausgabe"; -"Privacy" = "Privatsphäre"; +"Privacy" = "Datenschutz"; "Logging" = "Protokollierung"; "Enable logging" = "Protokollierung aktivieren"; @@ -206,8 +208,8 @@ "Warning" = "Warnung"; "Error" = "Fehler"; -"Share log file" = "Logdatei teilen"; -"Reset log file" = "Logdatei zurücksetzen"; +"Share log file" = "Protokolldatei teilen"; +"Reset log file" = "Protokolldatei zurücksetzen"; "When activated, logs may impact performance and include sensitive information. However the logs are not subject to automatic submission to %@ servers. Sharing logs with others is sole user responsibility." = "Wenn aktiviert, können Protokolle die Leistung beeinträchtigen und sensible Informationen enthalten. Die Protokolle unterliegen jedoch nicht der automatischen Übertragung an %@ Server. Die Weitergabe von Protokollen an andere liegt in der alleinigen Verantwortung des Benutzers."; @@ -221,13 +223,15 @@ "Enable log file" = "Protokolldatei aktivieren"; "Really reset log file?" = "Protokolldatei wirklich zurücksetzen?"; -"This action can't be undone." = "Diese Handlung kann nicht rückgängig gemacht werden."; +"This action can't be undone." = "Diese Aktion kann nicht rückgängig gemacht werden."; /* Storage settings */ "Delete unused local copies" = "Lösche ungenutzte lokale Kopien"; "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Gemessene Zeit seit dem Hochladen, Bearbeiten, Herunterladen oder Anzeigen der jeweiligen Datei über dieses Gerät. Gilt nicht für Dateien, die über die Funktion Offline verfügbar heruntergeladen wurden. Lokale Kopien können vor Ablauf des angegebenen Zeitraums gelöscht werden, z.B. weil es eine neuere Version einer Datei auf dem Server gibt - oder durch das manuelle Löschen von Offline-Kopien. Außerdem dürfen lokale Kopien nach Ablauf der angegebenen Zeit nicht gelöscht werden, z.B. wenn eine Aktion an ihr durchgeführt wird, die Datei noch in Gebrauch ist - oder das Konto, das die Datei enthält, nicht in der App verwendet wurde."; "never" = "Niemals"; "after %@" = "nach %@"; +"Decrease Slider Value" = "Decrease Slider Value"; +"Increase Slider Value" = "Increase Slider Value"; /* Display settings */ "Display settings" = "Anzeigeeinstellungen"; @@ -238,11 +242,11 @@ "Enter code" = "Code eingeben"; "Repeat code" = "Code wiederholen"; "Delete code" = "Code löschen"; -"The entered codes are different" = "Die eingegebenen Codes sind verschieden"; +"The entered codes are different" = "Die eingegebenen Codes sind unterschiedlich"; "Incorrect code" = "Falscher Code"; -"Please try again in %@" = "Bitte erneut in %@ versuchen"; +"Please try again in %@" = "Bitte nochmal in %@ versuchen"; "Unlock %@" = "%@ entsperren"; -"Biometric authentication failed" = "Biometrische Authentifizierung fehlgeschlagen"; +"Biometric authentication failed" = "Biometrische Authentifizierung ist fehlgeschlagen"; /* Certificate management */ @@ -251,12 +255,12 @@ "Approved" = "Geprüft"; "Auto-approved" = "Automatisch genehmigt"; -"Revoke approval" = "Bestätigung widerrufen"; +"Revoke approval" = "Genehmigung widerrufen"; /* Actions */ "Forbidden Characters" = "Nicht erlaubte Zeichen"; -"File name cannot contain / or \\" = "Dateiname darf nicht / oder \\ enthalten"; +"File name cannot contain / or \\" = "Dateiname darf nicht / oder \\ enthalten."; "New Folder" = "Neuer Ordner"; "Folder name" = "Ordnername"; "Rename" ="Umbenennen"; @@ -270,28 +274,31 @@ " couldn't download file(s)" = "Datei(en) konnte nicht geladen werden"; "Actions" = "Aktionen"; "copy" = "Kopie"; +"Close Window" = "Close Window"; +"Open in a new Window" = "Open in a new Window"; +"Open in Window" = "Open in Window"; "Preparing…" = "Vorbereiten..."; /* Directory Picker Messages */ -"Move here" = "Hierhin verschieben"; +"Move here" = "Hierher bewegen"; /* Preview */ "Open file" = "Datei öffnen"; "Network unavailable" = "Netzwerk nicht verfügbar"; "Error" = "Fehler"; "Could not get the picture" = "Bild konnte nicht geladen werden"; -"Downloading" = "Lade herunter"; +"Downloading" = "Herunterladen"; "File couldn't be opened" = "Datei kann nicht geöffnet werden"; /* PDF Viewer */ "Resume" = "Fortsetzen"; -"Go to page" = "Gehe zu Seite"; +"Go to page" = "Gehe zur Seite"; "Page" = "Seite"; "This document has %@ pages" = "Dieses Dokument hat %@ Seiten"; "%@ of %@" = "%@ von %@"; "Invalid Page" = "Ungültige Seite"; -"The entered page number doesn't exist" = "Die angegebene Seitennummer existiert nicht."; +"The entered page number doesn't exist" = "Die eingegebene Seitennummer existiert nicht"; "Search PDF" = "PDF suchen"; "Outline" = "Gliederung"; @@ -303,6 +310,18 @@ "Albums" = "Alben"; "Importing from photo library" = "Importieren aus der Fotobibliothek"; +/* Scan */ +"Scan" = "Scan"; +"Scans" = "Scans"; +"Scan document" = "Scan document"; +"Saving" = "Speichern"; +"File format" = "File format"; +"Name" = "Name"; +"Save as" = "Speichern unter"; +"Options" = "Optionen"; +"Scan additional" = "Scan additional"; +"Create one file per page" = "Create one file per page"; + /* Sharing */ "Searching Shares…" = "Suche nach Freigaben…"; "Recipient" = "Empfänger"; @@ -337,7 +356,7 @@ "Receive files from multiple recipients without revealing the contents of the folder." = "Von mehreren Empfängern Dateien empfangen, ohne den Inhalt des Ordners preiszugeben."; "Download / View" = "Herunterladen / Ansehen"; "Download / View / Upload" = "Herunterladen / Ansehen / Hochladen"; -"Upload only (File Drop)" = "Nur Hochladen (File Drop)"; +"Upload only (File Drop)" = "Nur hochladen (Datei hineinziehen)"; "Creating public link failed" = "Öffentlichen Link erstellen fehlgeschlagen"; "Create Public Link" = "Öffentlichen Link erstellen"; "Links" = "Links"; @@ -355,15 +374,15 @@ "Password Protected" = "passwortgeschützt"; "Pending Federated Invites" = "Ausstehende Federated-Einladungen"; "Pending Invites" = "Ausstehende Einladungen"; -"Shared with you" = "Mit Dir geteilt"; -"Shared with others" = "Von Dir geteilt"; -"Shares" = "Freigaben"; +"Shared with you" = "Mit Ihnen geteilt"; +"Shared with others" = "Mit anderen geteilt"; +"Shares" = "Geteiltes"; "Copy Private Link" = "Privaten Link kopieren"; "Only recipients can use this link. Use it as a permanent link to point to this resource" = "Nur Empfänger können diesen Link verwenden. Verwenden Sie diesen als permanenten Link, um auf diese Ressource zu verweisen."; "Accept Share failed" = "Freigabe annehmen fehlgeschlagen"; "Decline Share failed" = "Freigabe ablehnen fehlgeschlagen"; "Accept" = "Akzeptieren"; -"Decline" = "Abgelehnt"; +"Decline" = "Ablehnen"; "Declined" = "Abgelehnt"; "Decline Share" = "Freigabe ablehnen"; "Unshare" = "Freigabe aufheben"; @@ -395,7 +414,7 @@ "Recents" = "Kürzlich"; "Favorites"= "Favoriten"; "Images" = "Bilder"; -"PDF Documents" = "PDF-Dokumente"; +"PDF Documents" = "PDF Dokumente"; /* Media files settings */ "Media Files" = "Medien Dateien"; @@ -403,8 +422,16 @@ /* Photo upload settings */ "Media Upload" = "Medienupload"; -"Convert HEIC to JPEG" = "HEIC in JPEG konvertieren"; +"Convert HEIC to JPEG" = "Konvertiere HEIC zu JPEG"; "Convert videos to MP4" = "Videos konvertieren zu MP4"; +"Auto Upload Photos" = "Sofortiges hochladen der Bilder "; +"Auto Upload Videos" = "Sofortiges hochladen der Videos"; +"Account" = "Benutzerkonto"; +"Accounts" = "Konten"; +"Select account" = "Account auswählen"; +"Upload Path" = "Upload Pfad"; +"Select Upload Path" = "Upload-Pfad auswählen"; +"Auto upload of media was disabled since configured account / folder was not found" = "Auto Upload von Medien wurde deaktiviert, da das konfigurierte Konto / Ordner nicht gefunden wurde."; /* Progress summarizer */ "Creating %ld folders…" = "Erstelle %ld Ordner…"; @@ -424,7 +451,6 @@ "Delete Local Copies" = "Lösche lokale Kopien"; "Manage" = "Verwalten"; "Storage" = "Speicher"; -"Proceed" = "Fortfahren"; "Compacting" = "Freigeben"; "Really include available offline files?" = "Offline-Dateien wirklich einbeziehen?"; @@ -443,8 +469,27 @@ "Overview" = "Übersicht"; "All Files" = "Alle Dateien"; -"Locations" = "Speicherorte"; /* Import File */ "Save File" = "Datei speichern"; "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Wählen Sie ein Konto und einen Ordner, in den die Datei importiert werden soll.\n\nEs kann nur eine Datei auf einmal importiert werden."; + +/* Key Commands */ +"Select Next" = "Select Next"; +"Select Previous" = "Select Previous"; +"Open Selected" = "Open Selected"; +"Change Sort Order" = "Change Sort Order"; +"Search" = "Suche"; +"Back" = "Zurück"; +"Save" = "Speichern"; +"Sort by %@" = "Sortiert nach %@"; +"Tab %@" = "Tab %@"; +"Select Last Item on Page" = "Select Last Item on Page"; +"Scroll to Top" = "Scroll to Top"; +"Scroll to Bottom" = "Scroll to Bottom"; +"Copy to Pasteboard" = "Copy to Pasteboard"; +"Import from Pasteboard" = "Import from Pasteboard"; +"Next" = "Weiter"; +"Previous" = "Zurück"; +"Favorite" = "Favorit"; +"Cut" = "Cut"; diff --git a/ownCloud/Resources/el.lproj/Localizable.strings b/ownCloud/Resources/el.lproj/Localizable.strings index 5a182c4cf..d0fb3cc40 100644 Binary files a/ownCloud/Resources/el.lproj/Localizable.strings and b/ownCloud/Resources/el.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/en-GB.lproj/Localizable.strings b/ownCloud/Resources/en-GB.lproj/Localizable.strings index 7ca777beb..0259dc7c1 100644 --- a/ownCloud/Resources/en-GB.lproj/Localizable.strings +++ b/ownCloud/Resources/en-GB.lproj/Localizable.strings @@ -141,7 +141,6 @@ "Passcode Lock" = "Passcode Lock"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Instant Uploads"; "Videos" = "Videos"; "Photos" = "Photos"; "Background uploads" = "Background uploads"; diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index a37731d43..c523173f1 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -149,7 +149,6 @@ "Passcode Lock" = "Passcode Lock"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Instant Uploads"; "Videos" = "Videos"; "Photos" = "Photos"; "Background uploads" = "Background uploads"; @@ -310,6 +309,20 @@ "All Photos" = "All Photos"; "Albums" = "Albums"; "Importing from photo library" = "Importing from photo library"; +"Media import" = "Media import"; +"%@ of %@" = "%@ of %@"; + +/* Scan */ +"Scan" = "Scan"; +"Scans" = "Scans"; +"Scan document" = "Scan document"; +"Saving" = "Saving"; +"File format" = "File format"; +"Name" = "Name"; +"Save as" = "Save as"; +"Options" = "Options"; +"Scan additional" = "Scan additional"; +"Create one file per page" = "Create one file per page"; /* Scan */ "Scan" = "Scan"; @@ -425,14 +438,15 @@ "Media Upload" = "Media Upload"; "Convert HEIC to JPEG" = "Convert HEIC to JPEG"; "Convert videos to MP4" = "Convert videos to MP4"; -"Instant Upload Photos" = "Instant Upload Photos"; -"Instant Upload Videos" = "Instant Upload Videos"; +"Auti Upload Photos" = "Auto Upload Photos"; +"Auto Upload Videos" = "Auto Upload Videos"; "Account" = "Account"; "Accounts" = "Accounts"; "Select account" = "Select account"; "Upload Path" = "Upload Path"; "Select Upload Path" = "Select Upload Path"; -"Instant upload of media was disabled since configured account / folder was not found" = "Instant upload of media was disabled since configured account / folder was not found"; +"Auto upload of media was disabled since configured account / folder was not found" = "Auto upload of media was disabled since configured account / folder was not found"; +"Importing %@ media files for upload" = "Importing %@ media files for upload"; /* Progress summarizer */ "Creating %ld folders…" = "Creating %ld folders…"; diff --git a/ownCloud/Resources/es.lproj/Localizable.strings b/ownCloud/Resources/es.lproj/Localizable.strings index 9ead55d4b..59f0042f2 100644 Binary files a/ownCloud/Resources/es.lproj/Localizable.strings and b/ownCloud/Resources/es.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/eu.lproj/Localizable.strings b/ownCloud/Resources/eu.lproj/Localizable.strings index 0dee35594..2c1c5b0d1 100644 Binary files a/ownCloud/Resources/eu.lproj/Localizable.strings and b/ownCloud/Resources/eu.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/gl.lproj/InfoPlist.strings b/ownCloud/Resources/gl.lproj/InfoPlist.strings index 78e1445b1..90ff5f3a9 100644 Binary files a/ownCloud/Resources/gl.lproj/InfoPlist.strings and b/ownCloud/Resources/gl.lproj/InfoPlist.strings differ diff --git a/ownCloud/Resources/gl.lproj/Localizable.strings b/ownCloud/Resources/gl.lproj/Localizable.strings index 8e412a79a..36e830b44 100644 Binary files a/ownCloud/Resources/gl.lproj/Localizable.strings and b/ownCloud/Resources/gl.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/he.lproj/Localizable.strings b/ownCloud/Resources/he.lproj/Localizable.strings index 9551bb9c1..f0225e2e9 100644 Binary files a/ownCloud/Resources/he.lproj/Localizable.strings and b/ownCloud/Resources/he.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/ko.lproj/Localizable.strings b/ownCloud/Resources/ko.lproj/Localizable.strings index 28c317d33..30336c1c8 100644 --- a/ownCloud/Resources/ko.lproj/Localizable.strings +++ b/ownCloud/Resources/ko.lproj/Localizable.strings @@ -108,7 +108,6 @@ "Passcode Lock" = "Passcode Lock"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Instant Uploads"; "Videos" = "Videos"; "Photos" = "Photos"; "Background uploads" = "Background uploads"; diff --git a/ownCloud/Resources/mk.lproj/Localizable.strings b/ownCloud/Resources/mk.lproj/Localizable.strings index 28c317d33..30336c1c8 100644 --- a/ownCloud/Resources/mk.lproj/Localizable.strings +++ b/ownCloud/Resources/mk.lproj/Localizable.strings @@ -108,7 +108,6 @@ "Passcode Lock" = "Passcode Lock"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Instant Uploads"; "Videos" = "Videos"; "Photos" = "Photos"; "Background uploads" = "Background uploads"; diff --git a/ownCloud/Resources/nb-NO.lproj/Localizable.strings b/ownCloud/Resources/nb-NO.lproj/Localizable.strings index 28c317d33..30336c1c8 100644 --- a/ownCloud/Resources/nb-NO.lproj/Localizable.strings +++ b/ownCloud/Resources/nb-NO.lproj/Localizable.strings @@ -108,7 +108,6 @@ "Passcode Lock" = "Passcode Lock"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Instant Uploads"; "Videos" = "Videos"; "Photos" = "Photos"; "Background uploads" = "Background uploads"; diff --git a/ownCloud/Resources/nn-NO.lproj/Localizable.strings b/ownCloud/Resources/nn-NO.lproj/Localizable.strings index 28c317d33..30336c1c8 100644 --- a/ownCloud/Resources/nn-NO.lproj/Localizable.strings +++ b/ownCloud/Resources/nn-NO.lproj/Localizable.strings @@ -108,7 +108,6 @@ "Passcode Lock" = "Passcode Lock"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Instant Uploads"; "Videos" = "Videos"; "Photos" = "Photos"; "Background uploads" = "Background uploads"; diff --git a/ownCloud/Resources/pt-BR.lproj/Localizable.strings b/ownCloud/Resources/pt-BR.lproj/Localizable.strings index df179b79a..68b95ef91 100644 --- a/ownCloud/Resources/pt-BR.lproj/Localizable.strings +++ b/ownCloud/Resources/pt-BR.lproj/Localizable.strings @@ -124,6 +124,7 @@ "Sign in" = "Entrar"; "Ignore" = "Ignore"; "Continue offline" = "Continuar offline"; +"Media upload in the previous session was incomplete since the application was terminated" = "O envio de mídia na sessão anterior estava incompleto desde que o aplicativo foi encerrado"; /* Server List*/ "Cancel" = "Cancelar"; @@ -148,7 +149,6 @@ "Passcode Lock" = "Senha-código de bloqueio"; "Face ID" = "ID de Face"; "Touch ID" = "ID de toque"; -"Instant Uploads" = "Envios Instantâneos"; "Videos" = "Videos"; "Photos" = "Imagens"; "Background uploads" = "Atualização em segundo plano"; @@ -184,6 +184,8 @@ "Dark" = "Escuro"; "Light" = "Claro"; "Classic" = "Clássico"; +"System" = "Sistema"; +"System Appeareance" = "Aparência do Sistema"; /* Log settings */ "Log Files" = "Arquivos de log"; @@ -228,6 +230,8 @@ "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Tempo medido desde o envio, edição, baixa ou visualização do respectivo arquivo através deste dispositivo. Não se aplica a arquivos baixados pelo recurso Disponível Off-line. As cópias locais podem ser excluídas antes que o período de tempo tenha passado, por ex. porque há uma versão mais recente de um arquivo no servidor - ou através da exclusão manual de cópias offline. Além disso, as cópias locais não podem ser excluídas após o período de tempo especificado, por ex. se uma ação for executada, o arquivo ainda está em uso - ou a conta que o contém não foi usada no aplicativo. "; "never" = "nunca"; "after %@" = "após %@"; +"Decrease Slider Value" = "Decrescer o Valor de Recuo"; +"Increase Slider Value" = "Acrescer o Valor de Avanço"; /* Display settings */ "Display settings" = "Configurações do Visor"; @@ -270,6 +274,9 @@ " couldn't download file(s)" = "não foi possível baixar esse arquivo(s)"; "Actions" = "Ações"; "copy" = "copiar"; +"Close Window" = "Fechar Janela"; +"Open in a new Window" = "Abrir em uma nova Janela"; +"Open in Window" = "Abrir na Janela"; "Preparing…" = "Preparando…"; @@ -303,6 +310,18 @@ "Albums" = "Albuns"; "Importing from photo library" = "Importando da biblioteca de fotos"; +/* Scan */ +"Scan" = "Escanear"; +"Scans" = "Escaneamentos"; +"Scan document" = "Escanear documento"; +"Saving" = "Salvando"; +"File format" = "Formato do arquivo"; +"Name" = "Nome"; +"Save as" = "Salvar como"; +"Options" = "Opções"; +"Scan additional" = "Escaneamento adicional"; +"Create one file per page" = "Criar um aquivo por página"; + /* Sharing */ "Searching Shares…" = "Pesquisando Compartilhamentos..."; "Recipient" = "Destinatário"; @@ -405,6 +424,14 @@ "Media Upload" = "Envio de mídia"; "Convert HEIC to JPEG" = "Converter HEIC para JPEG"; "Convert videos to MP4" = "Converta vídeos para MP4"; +"Auto Upload Photos" = "Envio Instantâneo de Fotos"; +"Auto Upload Videos" = "Envio Instantâneo de Videos"; +"Account" = "Conta"; +"Accounts" = "Contas"; +"Select account" = "Selecionar conta"; +"Upload Path" = "Enviar Caminho"; +"Select Upload Path" = "Selecione Caminho para Envio"; +"Auto upload of media was disabled since configured account / folder was not found" = "O envio instantâneo de mídia foi desativado porque a conta/pasta configurada não foi encontrada"; /* Progress summarizer */ "Creating %ld folders…" = "Criando %ld pastas…"; @@ -424,6 +451,7 @@ "Delete Local Copies" = "Excluir Cópias Locais"; "Manage" = "Gerenciar"; "Storage" = "Armazenamento"; +"Compacting" = "Compactando"; "Really include available offline files?" = " Realmente incluir arquivos offline disponíveis?"; "Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = " Arquivos e pastas marcados como Disponível offline ficarão indisponíveis. Eles serão baixados novamente na próxima vez que você fizer login na sua conta (é necessária conectividade)."; @@ -445,3 +473,23 @@ /* Import File */ "Save File" = "Salvar Arquivo"; "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Escolha uma conta e uma pasta para importar o arquivo.\n\nApenas um arquivo pode ser importado de cada vez."; + +/* Key Commands */ +"Select Next" = "Selecionar Próximo"; +"Select Previous" = "Selecionar o Anterior"; +"Open Selected" = "Abrir o Selecionado"; +"Change Sort Order" = "Modificar Ordem de Classificação"; +"Search" = "Pesquisar"; +"Back" = "Voltar"; +"Save" = "Salvar"; +"Sort by %@" = "Ordenar por %@"; +"Tab %@" = "Aba %@"; +"Select Last Item on Page" = "Selecione Último item na Página"; +"Scroll to Top" = "Rolar para o Topo"; +"Scroll to Bottom" = "Rolar para Baixo"; +"Copy to Pasteboard" = "Copiar para o Segundo Plano"; +"Import from Pasteboard" = "Importar do Segundo Plano"; +"Next" = "Próxima"; +"Previous" = "Anterior"; +"Favorite" = "Favorito"; +"Cut" = "Cortar"; diff --git a/ownCloud/Resources/pt-PT.lproj/Localizable.strings b/ownCloud/Resources/pt-PT.lproj/Localizable.strings index 5cf3232ef..5edacfcec 100644 --- a/ownCloud/Resources/pt-PT.lproj/Localizable.strings +++ b/ownCloud/Resources/pt-PT.lproj/Localizable.strings @@ -98,7 +98,6 @@ "Passcode Lock" = "Código de Proteção"; "Face ID" = "Id. de Rosto"; "Touch ID" = "Id. por Toque"; -"Instant Uploads" = "Envios Instantâneos"; "Videos" = "Vídeos"; "Photos" = "Fotografias"; "Background uploads" = "Envios em segundo plano"; diff --git a/ownCloud/Resources/ru.lproj/Localizable.strings b/ownCloud/Resources/ru.lproj/Localizable.strings index 9977e8171..27cb7c6d9 100644 --- a/ownCloud/Resources/ru.lproj/Localizable.strings +++ b/ownCloud/Resources/ru.lproj/Localizable.strings @@ -124,6 +124,7 @@ "Sign in" = "Войти"; "Ignore" = "Игнорировать"; "Continue offline" = "Продолжить без сети"; +"Media upload in the previous session was incomplete since the application was terminated" = "Загрузка данных в прошлом сеансе не была завершена, так как приложение было завершено"; /* Server List*/ "Cancel" = "Отмена"; @@ -148,7 +149,6 @@ "Passcode Lock" = "Блокировка кодом доступа"; "Face ID" = "Идентификатор лица"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Мгновенные загрузки"; "Videos" = "Видео"; "Photos" = "Фото"; "Background uploads" = "Фоновые загрузки"; @@ -184,6 +184,8 @@ "Dark" = "Тёмная"; "Light" = "Светлая"; "Classic" = "Классическая"; +"System" = "Система"; +"System Appeareance" = "Внешний вид системы"; /* Log settings */ "Log Files" = "Лог-файлы"; @@ -228,6 +230,8 @@ "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Время, измеренное с момента закачки, правки, скачивания или просмотра соответствующего файла с помощью этого устройства. Не применяется к файлам, скачанным через возможность доступа без сети. Локальные копии могут удаляться до наступления заданного периода времени, например из-за более свежей версии файла на сервере, или через ручное удаление локальных копий. С другой стороны, локальные копии могут быть не удалены по истечении заданного времени, например из-за выполняемых над ними действий удерживающих файл, или учётная запись, в которой числится файл, не используется в приложении."; "never" = "никогда"; "after %@" = "после %@"; +"Decrease Slider Value" = "Увеличить значение слайдера"; +"Increase Slider Value" = "Уменьшить значение слайдера"; /* Display settings */ "Display settings" = "Установки отображения"; @@ -270,6 +274,9 @@ " couldn't download file(s)" = "не удалось скачать файл(ы)"; "Actions" = "Действия"; "copy" = "копия"; +"Close Window" = "Закрыть окно"; +"Open in a new Window" = "Открыть в новом окне"; +"Open in Window" = "Открыть в окне"; "Preparing…" = "Подготовка"; @@ -303,6 +310,18 @@ "Albums" = "Альбомы"; "Importing from photo library" = "Импорт из фото-библиотеки"; +/* Scan */ +"Scan" = "Cканировать"; +"Scans" = "Сканы"; +"Scan document" = "Сканировать документ"; +"Saving" = "Сохраняется"; +"File format" = "Формат файла"; +"Name" = "Название"; +"Save as" = "Сохранить как"; +"Options" = "Опции"; +"Scan additional" = "Сканировать дополнительно"; +"Create one file per page" = "Создавать по файлу на каждую страницу"; + /* Sharing */ "Searching Shares…" = "Поиск по общим ресурсам"; "Recipient" = "Получатель"; @@ -405,6 +424,14 @@ "Media Upload" = "Загрузка фото и видео"; "Convert HEIC to JPEG" = "Преобразовать HEIC в JPEG"; "Convert videos to MP4" = "Преобразовать видео в MP4"; +"Auto Upload Photos" = "Автоматически загружать фотоснимки"; +"Auto Upload Videos" = "Автоматически загружать видеоролики"; +"Account" = "Уч.запись"; +"Accounts" = "Учётные записи"; +"Select account" = "Выберите учётную запись"; +"Upload Path" = "Путь для загрузки"; +"Select Upload Path" = "Выберите путь для загрузки"; +"Auto upload of media was disabled since configured account / folder was not found" = "Автоматическая загрузка данных была отключена, так как настроенная учётная запись или папка не найдена"; /* Progress summarizer */ "Creating %ld folders…" = "Создание %ld папок…"; @@ -424,6 +451,7 @@ "Delete Local Copies" = "Удалить локальные копии"; "Manage" = "Управление"; "Storage" = "Хранилище"; +"Compacting" = "Сжатие"; "Really include available offline files?" = "Действительно включить доступные автономные файлы?"; "Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "Файлы и папки, помеченные как доступные без сети, станут недоступными. При следующем входе в учётную запись они будут скачаны повторно (нужна связь)."; @@ -445,3 +473,23 @@ /* Import File */ "Save File" = "Сохранить файл"; "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Выберите учётную запись и папку, куда импортировать файл.\n\nЗа раз можно импортировать только один файл."; + +/* Key Commands */ +"Select Next" = "Выбрать следующее"; +"Select Previous" = "Выбрать предыдущее"; +"Open Selected" = "Открыть выбранное"; +"Change Sort Order" = "Изменить порядок сортировки"; +"Search" = "Найти"; +"Back" = "Назад"; +"Save" = "Сохранить"; +"Sort by %@" = "Сортировка по %@"; +"Tab %@" = "Вкладка %@"; +"Select Last Item on Page" = "Выбрать последний элемент на странице"; +"Scroll to Top" = "Прокрутить в начало"; +"Scroll to Bottom" = "Прокрутить в конец"; +"Copy to Pasteboard" = "Копировать в буфер обмена"; +"Import from Pasteboard" = "Импорт из буфера обмена"; +"Next" = "Следующий"; +"Previous" = "Предыдущий"; +"Favorite" = "В избранное"; +"Cut" = "Вырезать"; diff --git a/ownCloud/Resources/sq.lproj/Localizable.strings b/ownCloud/Resources/sq.lproj/Localizable.strings index 57be90a0c..86f30e95f 100644 --- a/ownCloud/Resources/sq.lproj/Localizable.strings +++ b/ownCloud/Resources/sq.lproj/Localizable.strings @@ -124,6 +124,7 @@ "Sign in" = "Hyni"; "Ignore" = "Shpërfille"; "Continue offline" = "Vazhdo pa qenë në linjë"; +"Media upload in the previous session was incomplete since the application was terminated" = "Ngarkimi media në sesionin e mëparshëm qe i paplotë, ngaqë aplikacioni u mbyll"; /* Server List*/ "Cancel" = "Anuloje"; @@ -148,7 +149,6 @@ "Passcode Lock" = "Kyçje Me Kodkalim"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Ngarkime të Menjëhershme"; "Videos" = "Video"; "Photos" = "Foto"; "Background uploads" = "Ngarkime në prapaskenë"; @@ -184,6 +184,8 @@ "Dark" = "E errët"; "Light" = "E çelët"; "Classic" = "Klasike"; +"System" = "Sistem"; +"System Appeareance" = "Dukje Sistemi"; /* Log settings */ "Log Files" = "Kartela Regjistër"; @@ -228,6 +230,8 @@ "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Kohë e matur që prej ngarkimit, përpunimit, shkarkimit apo parjes së kartelës përkatëse përmes kësaj pajisjeje. Nuk prek kartelat e shkarkuara përmes veçorisë E passhme Jo Në Linjë. Kopjet vendore mund të fshihen përpara se të ketë kaluar periudha kohore e dhënë, p.sh., ngaqë në shërbyes pati një version më të ri të kartelës - ose përmes fshirjes dorazi të kopjeve offline. Kopjet vendore mundet gjithashtu të mos fshihen pasi të ketë kaluar periudha kohore e dhënë, p.sh., nëse mbi të po kryhet një veprim, kartela është ende në përdorim - ose llogaria që përmban kartelën s’është përdorur në aplikacion."; "never" = "kurrë"; "after %@" = "pas %@"; +"Decrease Slider Value" = "Zvogëloni Vlerë Rrëshqitësi"; +"Increase Slider Value" = "Zmadhoni Vlerë Rrëshqitësi"; /* Display settings */ "Display settings" = "Rregullime shfaqjeje"; @@ -270,6 +274,9 @@ " couldn't download file(s)" = "s’u shkarkua dot kartelë(a)"; "Actions" = "Veprime"; "copy" = "kopjoje"; +"Close Window" = "Mbylle Dritaren"; +"Open in a new Window" = "Hape në Dritare të re"; +"Open in Window" = "Hape në Dritare"; "Preparing…" = "Po përgatitet…"; @@ -303,6 +310,18 @@ "Albums" = "Albume"; "Importing from photo library" = "Importim nga fototekë"; +/* Scan */ +"Scan" = "Skanim"; +"Scans" = "Skanime"; +"Scan document" = "Skanoni dokument"; +"Saving" = "Po ruhet"; +"File format" = "Format kartelash"; +"Name" = "Emër"; +"Save as" = "Ruaje si"; +"Options" = "Mundësi"; +"Scan additional" = "Skanim shtesë"; +"Create one file per page" = "Krijo një kartelë për faqe"; + /* Sharing */ "Searching Shares…" = "Po kërkohet në Ndarje…"; "Recipient" = "Marrës"; @@ -405,6 +424,14 @@ "Media Upload" = "Ngarkim Media"; "Convert HEIC to JPEG" = "Shndërroje HEIC në JPEG"; "Convert videos to MP4" = "Shndërroji videot në MP4"; +"Auto Upload Photos" = "Ngarkoni Foto Në Çast"; +"Auto Upload Videos" = "Ngarkoni Video Në Çast"; +"Account" = "Llogari"; +"Accounts" = "Llogari"; +"Select account" = "Përzgjidhni llogari"; +"Upload Path" = "Shteg Ngarkimi"; +"Select Upload Path" = "Përzgjidhni Shteg Ngarkimi"; +"Auto upload of media was disabled since configured account / folder was not found" = "Ngarkimi i menjëhershëm i medias qe çaktivizuar, ngaqë s’u gjet llogari / dosje e formësuar "; /* Progress summarizer */ "Creating %ld folders…" = "Po krijohen %ld dosje…"; @@ -424,6 +451,7 @@ "Delete Local Copies" = "Fshi Kopje Vendore"; "Manage" = "Administroni"; "Storage" = "Depozitë"; +"Compacting" = "Po ngjeshet"; "Really include available offline files?" = "Të përfshihen vërtet kartela të passhme jashtë linje?"; "Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "Kartelat dhe dosjet me shenjë për Të Passhme Jashtë Linje s’do të mund të përdoren dot. Ato do të rishkarkohen herës pasuese që bëni hyrjen në llogarinë tuaj (lypset lidhje në internet)."; @@ -445,3 +473,23 @@ /* Import File */ "Save File" = "Ruaje Kartelën"; "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Zgjidhni një llogari dhe dosje ku të importohet kartela.\n\nMund të importohet vetëm një kartelë në herë."; + +/* Key Commands */ +"Select Next" = "Përzgjidhni Pasuesen"; +"Select Previous" = "Përzgjidhni të Mëparshmen"; +"Open Selected" = "Hap të Përzgjedhurën"; +"Change Sort Order" = "Ndrysho Rend Renditjeje"; +"Search" = "Kërko"; +"Back" = "Mbrapsht"; +"Save" = "Ruaje"; +"Sort by %@" = "Renditi sipas %@"; +"Tab %@" = "Skeda %@"; +"Select Last Item on Page" = "Përzgjidhni Elementin e Fundit në Faqe"; +"Scroll to Top" = "Rrëshqitni drejt Kreut"; +"Scroll to Bottom" = "Rrëshqitni drejt Fundit"; +"Copy to Pasteboard" = "Kopjoje në të Papastër"; +"Import from Pasteboard" = "Importo prej të Papastre"; +"Next" = "Pasuesja"; +"Previous" = "E mëparshmja"; +"Favorite" = "Vëre Si të Parapëlqyer"; +"Cut" = "Prije"; diff --git a/ownCloud/Resources/th-TH.lproj/Localizable.strings b/ownCloud/Resources/th-TH.lproj/Localizable.strings index e7694ad31..86f30e95f 100644 --- a/ownCloud/Resources/th-TH.lproj/Localizable.strings +++ b/ownCloud/Resources/th-TH.lproj/Localizable.strings @@ -17,431 +17,479 @@ */ /* Add / Edit Bookmark */ -"Edit account" = "แก้ไขบัญชี"; -"Add account" = "เพิ่มบัญชี"; -"Server URL" = "URL ของเซิร์ฟเวอร์"; +"Edit account" = "Përpunoni llogari"; +"Add account" = "Shtoni llogari"; +"Server URL" = "URL shërbyesi"; "https://example.com" = "https://example.com"; -"Continue" = "ดำเนินการต่อ"; -"Name" = "ชื่อ"; -"Example Server" = "เซิร์ฟเวอร์ตัวอย่าง"; -"Show Certificate Details" = "แสดงรายละเอียดใบรับรอง"; -"Connect" = "เชื่อมต่อ"; -"Delete Authentication Data" = "ลบข้อมูลการรับรองความถูกต้อง"; -"Authentication" = "รับรองความถูกต้อง"; -"Username" = "ชื่อผู้ใช้"; -"Password" = "รหัสผ่าน"; -"Certificate Details" = "รายละเอียดใบรับรอง"; -"Cancel" = "ยกเลิก"; -"Approve" = "อนุมัติ"; -"Error" = "ข้อผิดพลาด"; -"Review Connection" = "ตรวจสอบการเชื่อมต่อ"; -"Authenticated via" = "ตรวจสอบแล้วผ่านทาง"; -"Authenticated as %@ via %@" = "รับรองความถูกต้องด้วย %@ ผ่านทาง %@"; -"Edit" = "แก้ไข"; -"Credentials" = "ข้อมูลส่วนตัวสำหรับเข้าระบบ"; -"Rejected" = "ปฏิเสธแล้ว"; -"Passed" = "ผ่านแล้ว"; -"Accepted" = "ยืนยันแล้ว"; -"Validation Error" = "ข้อผิดพลาดในการตรวจสอบ"; -"Certificate was rejected by user." = "หนังสือรับรองถูกปฏิเสธโดยผู้ใช้"; -"Certificate has issues.\nOpen 'Certificate Details' for more informations." = "ใบรับรองมีปัญหา\nเปิด 'รายละเอียดใบรับรอง' สำหรับข้อมูลเพิ่มเติม"; -"No issues found. Certificate passed validation." = "ไม่พบปัญหา ผ่านการตรวจสอบใบรับรองแล้ว"; -"Certificate may have issues, but was accepted by user.\nOpen 'Certificate Details' for more informations." = "ใบรับรองอาจมีปัญหาแต่ได้รับการยอมรับจากผู้ใช้แล้ว\nเปิด 'รายละเอียดใบรับรอง' สำหรับข้อมูลเพิ่มเติม"; -"If you 'Continue', you will be prompted to allow the '%@' App to open OAuth2 login where you can enter your credentials." = "หากคุณ 'ดำเนินการต่อ' คุณจะได้รับแจ้งให้อนุญาตแอป '%@' เพื่อเข้าสู่ระบบ OAuth2 ที่คุณสามารถป้อนหนังสือรับรองของคุณ"; - -"Contacting server…" = "กำลังติดต่อเซิร์ฟเวอร์..."; -"Authenticating…" = "กำลังตรวจสอบสิทธิ์..."; - -"Fetching user information…" = "กำลังเรียกข้อมูลผู้ใช้..."; -"Updating connection…" = "กำลังอัปเดตการเชื่อมต่อ ..."; - -"Missing hostname" = "ไม่มีชื่อโฮสต์"; -"The entered URL does not include a hostname." = "ป้อน URL โดยไม่รวมชื่อโฮสต์"; -"Add account" = "เพิ่มบัญชี"; -"Server name" = "ชื่อเซิร์ฟเวอร์"; -"Server Password" = "รหัสผ่านเซิร์ฟเวอร์"; -"Server Username" = "ชื่อผู้ใช้เซิร์ฟเวอร์"; +"Continue" = "Vazhdo"; +"Name" = "Emër"; +"Example Server" = "Shërbyes Shembull"; +"Show Certificate Details" = "Shfaq Hollësi Dëshmie"; +"Connect" = "Lidhuni"; +"Delete Authentication Data" = "Fshi të Dhëna Mirëfilltësimi"; +"Authentication" = "Mirëfilltësim"; +"Username" = "Emër përdoruesi"; +"Password" = "Fjalëkalim"; +"Certificate Details" = "Hollësi Dëshmie"; +"Cancel" = "Anuloje"; +"Approve" = "Miratoje"; +"Error" = "Gabim"; +"Review Connection" = "Shqyrtoni Lidhjen"; +"Authenticated via" = "Mirëfilltësuar përmes"; +"Authenticated as %@ via %@" = "Mirëfilltësuar si %@ përmes %@"; +"Edit" = "Përpunoni"; +"Credentials" = "Kredenciale"; +"Rejected" = "Hedhur poshtë"; +"Passed" = "Vleftësuar"; +"Accepted" = "Pranuar"; +"Validation Error" = "Gabim Vleftësim"; +"Certificate was rejected by user." = "Dëshmia u hodh poshtë nga përdoruesi."; +"Certificate has issues.\nOpen 'Certificate Details' for more informations." = "Dëshmia ka probleme.\nPër më tepër të dhëna, hapni 'Hollësi Dëshmie'."; +"No issues found. Certificate passed validation." = "S’u gjetën probleme. Dëshmia e mori vleftësimin."; +"Certificate may have issues, but was accepted by user.\nOpen 'Certificate Details' for more informations." = "Dëshmia mund të ketë probleme, por u pranua nga përdoruesi.\nPër më tepër hollësi, hapni 'Hollësi Dëshmie'."; +"If you 'Continue', you will be prompted to allow the '%@' App to open OAuth2 login where you can enter your credentials." = "Nëse 'Vazhdoni', do t’ju shfaqet kërkesa për të lejuar aplikacionin '%@' të hapë hyrjet me OAuth2, prej nga mund të jepni kredencialet tuaja."; + +"Contacting server…" = "Po kontaktohet shërbyesi…"; +"Authenticating…" = "Po bëhet mirëfilltësimi…"; + +"Fetching user information…" = "Po sillen të dhëna përdoruesi…"; +"Updating connection…" = "Po përditësohet lidhja…"; + +"Missing hostname" = "Mungon strehëemër"; +"The entered URL does not include a hostname." = "URL-ja e dhënë nuk përmban një strehëemër."; +"Add account" = "Shtoni llogari"; +"Server name" = "Emër shërbyesi"; +"Server Password" = "Fjalëkalim Shërbyesi"; +"Server Username" = "Emër përdoruesi Shërbyesi"; /* Client */ -"Browse" = "เรียกดู"; -"Disconnect" = "ตัดการเชื่อมต่อ"; -"Connecting…" = "กำลังเชื่อมต่อ..."; -"Connected." = "เชื่อมต่อแล้ว"; -"Select" = "เลือก"; -"Done" = "เสร็จสิ้น"; - -"Folder" = "แฟ้มเอกสาร"; - -"Stopped" = "หยุดแล้ว"; -"Started…" = "เริ่มต้นแล้ว..."; -"Contents from cache." = "เนื้อหาจากแคช"; -"Waiting for server response…" = "กำลังรอการตอบกลับของเซิร์ฟเวอร์..."; -"This folder no longer exists." = "โฟลเดอร์นี้ไม่มีอยู่แล้ว"; -"Everything up-to-date." = "ทุกอย่างใหม่ล่าสุดแล้ว"; -"Please wait…" = "โปรดรอ…"; -"Sort by %@" = "จัดเรียงตาม %@"; -"Sort by" = "เรียงตาม"; - -"name" = "ชื่อ"; -"type" = "ชนิด"; -"size" = "ขนาด"; -"date" = "วันที่"; -"Search this folder" = "ค้นหาโฟลเดอร์นี้"; -"Pending" = "อยู่ระหว่างดำเนินการ"; -"Show parent paths" = "แสดงพาธ"; - -"%@ of %@ used" = "มีทั้งหมด %@ ใช้ไปแล้ว%@"; -"Total: %@" = "ทั้งหมด: %@"; +"Browse" = "Shfletoni"; +"Disconnect" = "Shkëputu"; +"Connecting…" = "Po lidhet…"; +"Connected." = "U lidh."; +"Select" = "Përzgjidhe"; +"Done" = "U bë"; + +"Folder" = "Dosje"; + +"Stopped" = "U ndal"; +"Started…" = "U fillua…"; +"Contents from cache." = "Lëndë prej fshehtine."; +"Waiting for server response…" = "Po pritet për përgjigje nga shërbyesi…"; +"This folder no longer exists." = "Kjo dosje s’ekziston më."; +"Everything up-to-date." = "Gjithçka është e përditësuar."; +"Please wait…" = "Ju lutemi, prisni…"; +"Sort by %@" = "Renditi sipas %@"; +"Sort by" = "Renditi sipas"; + +"name" = "emër"; +"type" = "lloj"; +"size" = "madhësi"; +"date" = "datë"; +"Search this folder" = "Kërko në këtë dosje"; +"Pending" = "Në pritje"; +"Show parent paths" = "Shfaq shtigjet mëmë"; + +"%@ of %@ used" = "%@ nga %@ të përdorur"; +"Total: %@" = "Gjithsej: %@"; /* Client Messages */ -"Empty folder" = "โฟลเดอร์ว่างเปล่า"; -"This folder contains no files or folders." = "โฟลเดอร์นี้ไม่มีไฟล์หรือโฟลเดอร์"; - -"Folder removed" = "ลบโฟลเดอร์แล้ว"; -"This folder no longer exists on the server." = "โฟลเดอร์นี้ไม่มีอยู่บนเซิร์ฟเวอร์อีกต่อไป"; -"Are you sure you want to delete this item from the server?" = "คุณแน่ใจว่าต้องการลบรายการนี้ออกจากเซิร์ฟเวอร์?"; -"Are you sure you want to delete these items from the server?" = "คุณแน่ใจหรือว่าต้องการลบรายการเหล่านี้ออกจากเซิร์ฟเวอร์?"; -"Multiple items" = "หลายรายการ"; -"Deleting '%@'" = "กำลังลบ '%@'"; -"Renaming to %@" = "กำลังเปลี่ยนฃื่อเป็น %@"; -"Upload from your photo library" = "อัปโหลดจากคลังรูปภาพของคุณ"; -"Missing permissions" = "ไม่มีสิทธิ์"; -"This permission is needed to upload photos and videos from your photo library." = "จำเป็นต้องได้รับอนุญาตเพื่ออัปโหลดรูปภาพและวิดีโอจากคลังรูปภาพของคุณ"; -"Not now" = "ไม่ใช่ตอนนี้"; -"Upload file" = "อัปโหลดไฟล์"; -"Upload files" = "อัปโหลดไฟล์"; - -"No matches" = "ไม่ตรงกัน"; -"There are no results for this search term" = "ไม่มีผลลัพธ์สำหรับคำค้นหานี้"; -"Status" = "สถานะ"; - -"Authorization failed" = "การอนุมัติล้มเหลว"; -"The account has been disabled." = "บัญชีถูกปิดใช้งาน"; -"The server declined access with the credentials stored for this connection." = "เซิร์ฟเวอร์ปฏิเสธการเข้าถึงด้วยหนังสือรับรองที่เก็บไว้สำหรับการเชื่อมต่อนี้"; -"Access denied" = "เข้าถึงถูกปฏิเสธ"; -"The connection's access token has expired or become invalid. Sign in again to re-gain access." = "โทเค็นเข้าถึงการเชื่อมต่อหมดอายุหรือไม่ถูกต้อง กรุณาลงชื่อเข้าใช้อีกครั้งเพื่อรับสิทธิ์การเข้าถึง"; -"No authentication data has been found for this connection." = "ไม่พบข้อมูลการตรวจสอบสิทธิ์สำหรับการเชื่อมต่อนี้"; -"Sign in" = "เข้าสู่ระบบ"; -"Ignore" = "ไม่สนใจ"; -"Continue offline" = "ใช่งานแบบออฟไลน์ต่อไป"; +"Empty folder" = "Dosje e zbrazët"; +"This folder contains no files or folders." = "Kjo dosje s’përmban kartela ose dosje."; + +"Folder removed" = "Dosje e hequr"; +"This folder no longer exists on the server." = "Kjo dosje s’ekziston më te shërbyesi."; +"Are you sure you want to delete this item from the server?" = "Jeni i sigurt se doni të fshihet ky objekt prej shërbyesit?"; +"Are you sure you want to delete these items from the server?" = "Jeni i sigurt se doni të fshihen këto objekte prej shërbyesi?"; +"Multiple items" = "Shumë objekte"; +"Deleting '%@'" = "Po fshihet '%@'"; +"Renaming to %@" = "Po riemërtohet si %@"; +"Upload from your photo library" = "Ngarkoni që nga fototeka juaj"; +"Missing permissions" = "Mungojnë leje"; +"This permission is needed to upload photos and videos from your photo library." = "Kjo leje është e domosdoshme për të ngarkuar foto dhe video prej fototekës tuaj."; +"Not now" = "Jo tani"; +"Upload file" = "Ngarkoni kartelë"; +"Upload files" = "Ngarko kartela"; + +"No matches" = "S’ka përputhje"; +"There are no results for this search term" = "S’ka përfundime për këtë term kërkimi"; +"Status" = "Gjendje"; + +"Authorization failed" = "Autorizimi dështoi"; +"The account has been disabled." = "Llogaria është çaktivizuar."; +"The server declined access with the credentials stored for this connection." = "Shërbyesi nuk pranoi hyrje me kredencialet e depozituara për këtë lidhje."; +"Access denied" = "Hyrje e mohuar"; +"The connection's access token has expired or become invalid. Sign in again to re-gain access." = "Token-i i hyrjes për lidhjen ka skaduar ose është bërë i pavlefshëm. Ribëni hyrjen në llogari, që të rifitoni hyrje."; +"No authentication data has been found for this connection." = "Për këtë lidhje s’u gjetën të dhëna mirëfilltësimi."; +"Sign in" = "Hyni"; +"Ignore" = "Shpërfille"; +"Continue offline" = "Vazhdo pa qenë në linjë"; +"Media upload in the previous session was incomplete since the application was terminated" = "Ngarkimi media në sesionin e mëparshëm qe i paplotë, ngaqë aplikacioni u mbyll"; /* Server List*/ -"Cancel" = "ยกเลิก"; -"OK" = "ตกลง"; - -"'%@' is currently locked" = "'%@' ปัจจุบันถูกล็อกอยู่"; -"An operation is currently performed that prevents connecting to '%@'. Please try again later." = "ขณะนี้มีการดำเนินการเพื่อป้องกันการเชื่อมต่อไปยัง '%@' กรุณาลองใหม่อีกครั้งในภายหลัง"; -"Really delete '%@'?" = "ต้องการลบ '%@'?"; -"This will also delete all locally stored file copies." = "นอกจากนี้จะลบไฟล์สำเนาที่เก็บไว้ในเครื่องทั้งหมด"; -"Delete" = "ลบ"; -"Deletion of '%@' failed" = "การลบ '%@' ล้มเหลว"; -"Accounts" = "บัญชี"; - -"Help" = "ช่วยเหลือ"; -"Feedback" = "ข้อเสนอแนะ"; -"Welcome" = "ยินดีต้อนรับ"; -"Thanks for choosing %@! \n Start by adding your account." = "ขอบคุณที่เลือก %@!\nเริ่มต้นด้วยการเพิ่มบัญชีของคุณ"; +"Cancel" = "Anuloje"; +"OK" = "OK"; + +"'%@' is currently locked" = "'%@' është e kyçur"; +"An operation is currently performed that prevents connecting to '%@'. Please try again later." = "Po kryhet një veprim që e pengon lidhjen me '%@'. Ju lutemi, riprovoni më vonë."; +"Really delete '%@'?" = "Të fshihet '%@' vërtet?"; +"This will also delete all locally stored file copies." = "Kjo do të shkaktojë fshirjen edhe të krejt kopjeve të kartelës të depozituara lokalisht."; +"Delete" = "Fshije"; +"Deletion of '%@' failed" = "Fshirja e '%@' dështoi"; +"Accounts" = "Llogari"; + +"Help" = "Ndihmë"; +"Feedback" = "Përshtypje"; +"Welcome" = "Mirë se vini"; +"Thanks for choosing %@! \n Start by adding your account." = "Faleminderit që zgjodhët %@! \nFillojani duke shtuar llogarinë tuaj."; /* Settings Messages */ -"Security" = "ความปลอดภัย"; -"Passcode Lock" = "ล็อกรหัสผ่าน"; -"Face ID" = "ปลดล็อคด้วยใบหน้า"; -"Touch ID" = "ยืนยันตนด้วยลายนิ้วมือ"; -"Instant Uploads" = "อัปโหลดทันที"; -"Videos" = "วิดีโอ"; -"Photos" = "รูปภาพ"; -"Background uploads" = "อัปโหลดพื้นหลัง"; -"Wifi only" = "Wifi เท่านั้น"; -"Settings" = "ตั้งค่า"; -"Lock application" = "ล็อกแอปพลิเคชัน"; -"Upload videos via WiFi only" = "อัปโหลดวีดีโอผ่านทาง WiFi เท่านั้น"; -"Upload pictures via WiFi only" = "อัปโหลดรูปภาพผ่านทาง WiFi เท่านั้น"; -"More" = "ขยาย"; -"Send feedback" = "ส่งความคิดเห็น"; -"Recommend to a friend" = "แนะนำให้เพื่อน"; -"Privacy Policy" = "นโยบายส่วนบุคคล"; -"Acknowledgements" = "กิตติกรรมประกาศ"; -"Video upload path" = "เส้นทางการอัปโหลดวิดีโอ"; -"Photo upload path" = "เส้นทางการอัปโหลดภาพ"; -"Immediately" = "ทันที"; -"After 1 minute" = "หลังจากผ่านไป 1 นาที"; -"After 5 minutes" = "หลังจากผ่านไป 5 นาที"; -"After 30 minutes" = "หลังจากผ่านไป 30 นาที"; -"If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.\n\nAccess in Files.app is not possible, if you choose lock interval \"Immediately\".\nPlease choose an other delay, if you want to access your files in the Files.app or via a document picker." = "หากคุณเลือก \"ทันที\" แอปจะถูกล็อคเมื่อไม่ถูกใช้งาน\n\nการเข้าถึงใน Files.app นั้นเป็นไปไม่ได้หากคุณเลือกช่วงเวลาล็อค \"ทันที\"\nโปรดเลือกความล่าช้าอื่นๆ หากคุณต้องการเข้าถึงไฟล์ของคุณใน Files.app หรือผ่านตัวเลือกเอกสาร"; -"Please configure an email account" = "โปรดกำหนดค่าบัญชีอีเมล"; -"You need to configure an email account first to be able to send emails." = "คุณต้องกำหนดค่าบัญชีอีเมลก่อนจึงจะสามารถส่งอีเมลได้"; -"Do you want to open the following URL?" = "คุณต้องการเปิด URL ต่อไปนี้?"; - -"%@ %@ version %@ build %@ (app: %@, sdk: %@)" = "%@%@รุ่น%@ บิวด์ %@ (แอป: %@, sdk: %@)"; -"beta" = "รุ่นเบต้า"; -"release" = "ล่าสุด"; +"Security" = "Siguri"; +"Passcode Lock" = "Kyçje Me Kodkalim"; +"Face ID" = "Face ID"; +"Touch ID" = "Touch ID"; +"Videos" = "Video"; +"Photos" = "Foto"; +"Background uploads" = "Ngarkime në prapaskenë"; +"Wifi only" = "Vetëm wifi"; +"Settings" = "Rregullime"; +"Lock application" = "Kyçe aplikacionin"; +"Upload videos via WiFi only" = "Videot ngarkoji vetëm përmes WiFi-it"; +"Upload pictures via WiFi only" = "Fotot ngarkoji vetëm përmes WiFi-it"; +"More" = "Më tepër"; +"Send feedback" = "Dërgoni përshtypje"; +"Recommend to a friend" = "Këshillojani një shoku"; +"Privacy Policy" = "Rregulla Privatësie"; +"Acknowledgements" = "Falënderime"; +"Video upload path" = "Shteg ngarkimesh videosh"; +"Photo upload path" = "Shteg ngarkimi fotosh"; +"Immediately" = "Menjëherë"; +"After 1 minute" = "Pas 1 minute"; +"After 5 minutes" = "Pas 5 minutash"; +"After 30 minutes" = "Pas 30 minutash"; +"If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.\n\nAccess in Files.app is not possible, if you choose lock interval \"Immediately\".\nPlease choose an other delay, if you want to access your files in the Files.app or via a document picker." = "Nëse zgjidhni \"Menjëherë\", Aplikacioni do të kyçet, kur të mos jetë më i dukshëm te pjesa e përparme.\n\nHyrja te Files.app s’është e mundur, nëse zgjidhni \"Menjëherë\" si interval kyçjeje.\nNëse doni të hyni te kartelat tuaja në Files.app ose përmes një zgjedhësi dokumentesh, ju lutemi, zgjidhni një tjetër vonesë."; +"Please configure an email account" = "Ju lutemi, formësoni një llogari email"; +"You need to configure an email account first to be able to send emails." = "Së pari lypset të formësoni një llogari email, që të jeni në gjendje të dërgoni email-e."; +"Do you want to open the following URL?" = "Doni të hapet URL-ja vijuese?"; + +"%@ %@ version %@ build %@ (app: %@, sdk: %@)" = "%@ %@ version %@ montim %@ (aplikacion: %@, sdk: %@)"; +"beta" = "beta"; +"release" = "hedhje në qarkullim"; /* User Interface Settings */ -"Theme" = "ธีม"; -"User Interface" = "หน้าจอผู้ใช้"; -"Dark" = "มืด"; -"Light" = "สว่าง"; -"Classic" = "คลาสสิก"; +"Theme" = "Temë"; +"User Interface" = "Ndërfaqe Përdoruesi"; +"Dark" = "E errët"; +"Light" = "E çelët"; +"Classic" = "Klasike"; +"System" = "Sistem"; +"System Appeareance" = "Dukje Sistemi"; /* Log settings */ -"Log Files" = "ไฟล์ log"; -"Browse log files" = "เรียกดูไฟล์ log"; -"Share" = "แชร์"; -"Delete all log files?" = "ลบไฟล์ log ทั้งหมด?"; -"Delete all" = "ลบทั้งหมด"; +"Log Files" = "Kartela Regjistër"; +"Browse log files" = "Shfletoni kartela regjistër"; +"Share" = "Ndaje me të tjerë"; +"Delete all log files?" = "Të fshihen krejt kartelat regjistër?"; +"Delete all" = "Fshiji krejt"; -"Log Level" = "ระดับ log"; -"Log Destinations" = "log ปลายทาง"; -"Privacy" = "ความเป็นส่วนตัว"; +"Log Level" = "Nivel Regjistrimi"; +"Log Destinations" = "Destinacione Regjistri"; +"Privacy" = "Privatësi"; -"Logging" = "กำลังเก็บบันทึก"; -"Enable logging" = "เปิดใช้ระบบบันทึกข้อมูล"; -"Options" = "ตัวเลือก"; +"Logging" = "Regjistrim"; +"Enable logging" = "Aktivizo regjistrimet"; +"Options" = "Mundësi"; -"Off" = "ปิด"; -"Info" = "ข้อมูล"; -"Default" = "ค่าเริ่มต้น"; -"Warning" = "คำเตือน"; -"Error" = "ข้อผิดพลาด"; +"Off" = "Off"; +"Info" = "Të dhëna"; +"Default" = "Parazgjedhje"; +"Warning" = "Kujdes"; +"Error" = "Gabim"; -"Share log file" = "แชร์ไฟล์ log"; -"Reset log file" = "รีเซ็ตไฟล์ log"; +"Share log file" = "Ndajeni kartelën e regjistrimit me dikë"; +"Reset log file" = "Zeroje kartelën e regjistrimit"; -"When activated, logs may impact performance and include sensitive information. However the logs are not subject to automatic submission to %@ servers. Sharing logs with others is sole user responsibility." = "เมื่อเปิดใช้งาน log อาจส่งผลต่อประสิทธิภาพและรวมถึงข้อมูลส่วนตัว อย่างไรก็ตาม log จะไม่ถูกส่งไปยังเซิร์ฟเวอร์ %@ โดยอัตโนมัติ การแชร์ log กับผู้อื่นถือเป็นความรับผิดชอบของผู้ใช้เพียงผู้เดียว"; +"When activated, logs may impact performance and include sensitive information. However the logs are not subject to automatic submission to %@ servers. Sharing logs with others is sole user responsibility." = "Kur aktivizohen, regjistrat mund të kenë ndikim në funksionim dhe përfshijnë të dhëna rezervat. Sidoqoftë, regjistrat nuk janë subjekt parashtrimi të automatizuar te shërbyes %@. Ndarja e regjistrave me të tjerë është përgjegjësi vetëm e përdoruesit."; -"The last 10 archived logs are kept on the device - with each log covering up to 24 hours of usage. When sharing please bear in mind that logs may contain sensitive information such as server URLs and user-specific information." = "log 10 ไฟล์ล่าสุดถูกเก็บไว้ในอุปกรณ์ - โดยแต่ละ log ครอบคลุมการใช้งานสูงสุด 24 ชั่วโมง เมื่อทำการแชร์โปรดจำไว้ว่า log อาจมีข้อมูลที่ละเอียดอ่อนเช่น URL เซิร์ฟเวอร์และข้อมูลเฉพาะของผู้ใช้"; +"The last 10 archived logs are kept on the device - with each log covering up to 24 hours of usage. When sharing please bear in mind that logs may contain sensitive information such as server URLs and user-specific information." = "10 regjistrat e fundit të arkivuar mbahen në pajisje - ku çdo regjistër mbulon deri në 24 orë përdorim. Kur bëhet ndarje e tyre, ju lutemi, kini parasysh se regjistrat mund të përmbajnë të dhëna rezervat, të tilla si URL shërbyesi dhe të dhëna që lidhen me përdoruesin."; -"Mask private data" = "ซ่อนข้อมูลส่วนตัว"; -"Enabling this option will attempt to mask private data, so it does not become part of any log. Since logging is a development and debugging feature, though, we can't guarantee that the log file will be free of any private data even with this option enabled. Therefore, please look through any log file and verify its free of any data you're not comfortable sharing before sharing it with anybody." = "การเปิดใช้งานตัวเลือกนี้จะพยายามปกปิดข้อมูลส่วนตัวดังนั้นจึงไม่มีการเก็บบันทึกใดๆ เนื่องจาก log เป็นคุณลักษณะการพัฒนาและดีบัก แม้ว่าจะเปิดใช้งานตัวเลือกนี้เราไม่สามารถรับประกันได้ว่าไฟล์ log จะไม่มีข้อมูลส่วนตัวใดๆ ดังนั้นโปรดดูไฟล์ log เพื่อความแน่ใจ"; +"Mask private data" = "Masko të dhëna private"; +"Enabling this option will attempt to mask private data, so it does not become part of any log. Since logging is a development and debugging feature, though, we can't guarantee that the log file will be free of any private data even with this option enabled. Therefore, please look through any log file and verify its free of any data you're not comfortable sharing before sharing it with anybody." = "Me aktivizimin e kësaj mundësie programi do të rreket të maskojë të dhëna private, që të mos jenë pjesë e ndonjë regjistrimi. Por, ngaqë regjistrimi është një veçori e lidhur me zhvillimin dhe diagnostikimet, s’mund të garantojmë që kartela regjistër do të jetë pa ndonjë të dhënë private, edhe pse kjo mundësi është e aktivizuar. Ndaj, ju lutemi, hidhini një sy cilësdo kartele regjistër dhe verifikoni se është pa ndonjë të dhënë për të cilën s’ndiheni rehat ta ndani me të tjerë, përpara se ta ndani me dikë."; -"No log file found" = "ไม่พบไฟล์ log"; -"The log file can't be shared because no log file could be found or the log file is empty." = "ไม่สามารถแชร์ไฟล์ log ได้เนื่องจากไม่พบไฟล์ log หรือไม่มีข้อมูล"; -"Enable log file" = "เปิดใช้งานไฟล์ log"; +"No log file found" = "S’u gjet kartelë regjistër"; +"The log file can't be shared because no log file could be found or the log file is empty." = "S’ndahet dot me të tjerë kartela regjistër, ngaqë s’u gjet e tillë, ose kartela regjistër është e zbrazët."; +"Enable log file" = "Aktivizo kartelë regjistër"; -"Really reset log file?" = "รีเซ็ตไฟล์ log?"; -"This action can't be undone." = "ไม่สามารถยกเลิกการดำเนินการนี้ได้"; +"Really reset log file?" = "Të zerohet vërtet kartela regjistër?"; +"This action can't be undone." = "Ky veprim s’mund të zhbëhet."; /* Storage settings */ -"Delete unused local copies" = "ลบสำเนาที่ไม่ได้ใช้ในเครื่อง"; -"Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "เวลาที่วัดได้ตั้งแต่การอัปโหลด แก้ไขดาวน์โหลด หรือดูไฟล์ที่เกี่ยวข้องผ่านอุปกรณ์นี้ ใช้ไม่ได้กับไฟล์ที่ดาวน์โหลดผ่านคุณสมบัติ \"การพร้อมใช้งานแบบออฟไลน์\" สำเนาในเครื่องอาจถูกลบก่อนการประสานข้อมูล เนื่องจากมีไฟล์เวอร์ชันใหม่บนเซิร์ฟเวอร์ - หรือผ่านการลบสำเนาออฟไลน์ด้วยตนเอง นอกจากนี้สำเนาในเครื่องอาจไม่ถูกลบหลังจากผ่านช่วงเวลาที่กำหนด หากมีการดำเนินการกับไฟล์นั้นยังคงมีการใช้งานอยู่หรือบัญชีที่ถือครองไฟล์นั้นไม่ได้ถูกใช้ในแอป"; -"never" = "ไม่ต้องเลย"; -"after %@" = "หลังจาก %@"; +"Delete unused local copies" = "Fshi kopje vendore të papërdorura"; +"Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Kohë e matur që prej ngarkimit, përpunimit, shkarkimit apo parjes së kartelës përkatëse përmes kësaj pajisjeje. Nuk prek kartelat e shkarkuara përmes veçorisë E passhme Jo Në Linjë. Kopjet vendore mund të fshihen përpara se të ketë kaluar periudha kohore e dhënë, p.sh., ngaqë në shërbyes pati një version më të ri të kartelës - ose përmes fshirjes dorazi të kopjeve offline. Kopjet vendore mundet gjithashtu të mos fshihen pasi të ketë kaluar periudha kohore e dhënë, p.sh., nëse mbi të po kryhet një veprim, kartela është ende në përdorim - ose llogaria që përmban kartelën s’është përdorur në aplikacion."; +"never" = "kurrë"; +"after %@" = "pas %@"; +"Decrease Slider Value" = "Zvogëloni Vlerë Rrëshqitësi"; +"Increase Slider Value" = "Zmadhoni Vlerë Rrëshqitësi"; /* Display settings */ -"Display settings" = "แสดงการตั้งค่า"; -"Show hidden files and folders" = "แสดงไฟล์และโฟลเดอร์ที่ซ่อน"; +"Display settings" = "Rregullime shfaqjeje"; +"Show hidden files and folders" = "Shfaq kartela dhe dosje të fshehura"; /* Passcode Messages */ -"Enter code" = "ใส่รหัส"; -"Repeat code" = "ใส่รหัสอีกครั้ง"; -"Delete code" = "ลบรหัส"; -"The entered codes are different" = "กรุณาป้อนรหัสให้ตรงกัน"; -"Incorrect code" = "รหัสไม่ถูกต้อง"; -"Please try again in %@" = "โปรดลองอีกครั้งใน %@"; -"Unlock %@" = "ปลดล็อค %@"; -"Biometric authentication failed" = "การตรวจสอบสิทธิ์แบบไบโอเมตริกซ์ล้มเหลว"; +"Enter code" = "Jepni kod"; +"Repeat code" = "Përsëriteni kodin"; +"Delete code" = "Fshije kodin"; +"The entered codes are different" = "Kodet e dhënë janë të ndryshëm"; +"Incorrect code" = "Kod i pasaktë"; +"Please try again in %@" = "Ju lutemi, riprovoni për %@"; +"Unlock %@" = "Shkyçe %@"; +"Biometric authentication failed" = "Mirëfilltësimi biometrik dështoi"; /* Certificate management */ -"Certificates" = "ใบรับรอง"; -"User-approved certificates" = "ใบรับรองที่ได้รับอนุญาตจากผู้ใช้"; +"Certificates" = "Dëshmi"; +"User-approved certificates" = "Dëshmi të miratuara nga përdorues"; -"Approved" = "ได้รับการอนุมัติ"; -"Auto-approved" = "อนุมัติอัตโนมัติ"; -"Revoke approval" = "เพิกถอนการอนุมัติ"; +"Approved" = "Të miratuara"; +"Auto-approved" = "E vetëmiratuar"; +"Revoke approval" = "Shfuqizoji miratimin"; /* Actions */ -"Forbidden Characters" = "ห้ามใช้อักขระต้องห้าม"; -"File name cannot contain / or \\" = "ชื่อไฟล์จะต้องไม่มี / หรือ \\"; -"New Folder" = "โฟลเดอร์ใหม่"; -"Folder name" = "ชื่อโฟลเดอร์"; -"Rename" ="เปลี่ยนชื่อ"; -"Create folder" ="สร้างโฟลเดอร์"; -"Duplicate" = "ซ้ำ"; -"Move" = "ย้าย"; -"Open in" = "เปิดใน"; -"Copy" = "คัดลอก"; -"Copy here" = "ึคัดลอก"; -"Cannot connect to " = "ไม่สามารถเชื่อมต่อ"; -" couldn't download file(s)" = "ไม่สามารถดาวน์โหลดไฟล์"; -"Actions" = "การกระทำ"; -"copy" = "คัดลอก"; - -"Preparing…" = "กำลังจัดเตรียม…"; +"Forbidden Characters" = "Shenja të Ndaluara"; +"File name cannot contain / or \\" = "Emri i kartelës s’mund të përmbajë / ose \\"; +"New Folder" = "Dosje e Re"; +"Folder name" = "Emër dosjeje"; +"Rename" ="Riemërtoje"; +"Create folder" ="Krijo dosje"; +"Duplicate" = "Përsëdyte"; +"Move" = "Zhvendose"; +"Open in" = "Hape në"; +"Copy" = "Kopjoje"; +"Copy here" = "Kopjoje këtu"; +"Cannot connect to " = "S’lidhet dot te"; +" couldn't download file(s)" = "s’u shkarkua dot kartelë(a)"; +"Actions" = "Veprime"; +"copy" = "kopjoje"; +"Close Window" = "Mbylle Dritaren"; +"Open in a new Window" = "Hape në Dritare të re"; +"Open in Window" = "Hape në Dritare"; + +"Preparing…" = "Po përgatitet…"; /* Directory Picker Messages */ -"Move here" = "ย้ายมาที่นี่"; +"Move here" = "Shpjere këtu"; /* Preview */ -"Open file" = "เปิดไฟล์"; -"Network unavailable" = "เครือข่ายใช้งานไม่ได้"; -"Error" = "ข้อผิดพลาด"; -"Could not get the picture" = "ไม่สามารถรับรูปภาพได้"; -"Downloading" = "กำลังดาวน์โหลด"; -"File couldn't be opened" = "ไม่สามารถเปิดไฟล์ได้"; +"Open file" = "Hap kartelë"; +"Network unavailable" = "Rrjet i pakapshëm"; +"Error" = "Gabim"; +"Could not get the picture" = "S’u mor dot figura"; +"Downloading" = "Shkarkim"; +"File couldn't be opened" = "Kartela s’u hap dot"; /* PDF Viewer */ -"Resume" = "ดำเนินการต่อ"; -"Go to page" = "ไปยังหน้า"; -"Page" = "หน้า"; -"This document has %@ pages" = "เอกสารนี้มีทั้งหมด %@ หน้า"; -"%@ of %@" = "%@ จาก %@"; -"Invalid Page" = "หน้าไม่ถูกต้อง"; -"The entered page number doesn't exist" = "ไม่มีหมายเลขหน้าที่ป้อน"; -"Search PDF" = "ค้นหา PDF"; -"Outline" = "เค้าโครง"; +"Resume" = "Rimerre"; +"Go to page" = "Shko tek faqja"; +"Page" = "Faqe"; +"This document has %@ pages" = "Ky dokument ka %@ faqe"; +"%@ of %@" = "%@ nga %@"; +"Invalid Page" = "Faqe e Pavlefshme"; +"The entered page number doesn't exist" = "Numri i dhënë për faqen nuk ekziston"; +"Search PDF" = "Kërko në PDF"; +"Outline" = "Përvijoje"; /* Photo Upload */ -"Upload" = "อัปโหลด"; -"Select All" = "เลือกทั้งหมด"; -"Deselect All" = "ยกเลิกการเลือกทั้งหมด"; -"All Photos" = "รูปภาพทั้งหมด"; -"Albums" = "อัลบัม"; -"Importing from photo library" = "นำเข้าจากคลังรูปภาพ"; +"Upload" = "Ngarkoje"; +"Select All" = "Përzgjidhi Krejt"; +"Deselect All" = "Shpërzgjidhi Krejt"; +"All Photos" = "Krejt Fotot"; +"Albums" = "Albume"; +"Importing from photo library" = "Importim nga fototekë"; + +/* Scan */ +"Scan" = "Skanim"; +"Scans" = "Skanime"; +"Scan document" = "Skanoni dokument"; +"Saving" = "Po ruhet"; +"File format" = "Format kartelash"; +"Name" = "Emër"; +"Save as" = "Ruaje si"; +"Options" = "Mundësi"; +"Scan additional" = "Skanim shtesë"; +"Create one file per page" = "Krijo një kartelë për faqe"; /* Sharing */ -"Searching Shares…" = "กำลังค้นหาแชร์..."; -"Recipient" = "ผู้รับ"; -"Recipients" = "ผู้รับ"; -"Public Link" = "ลิงก์สาธารณะ"; -"Public Links" = "ลิงก์สาธารณะ"; -"Shared by %@" = "แชร์แล้วโดย %@"; -"Invite Recipient" = "คำเชิญผู้รับ"; -"Recipients" = "ผู้รับ"; -"Add email or name" = "เพิ่มอีเมลหรือชื่อ"; -"Users" = "ผู้ใช้งาน"; -"Groups" = "กลุ่ม"; -"Start typing to search users, groups and remote users." = "เริ่มพิมพ์เพื่อค้นหาผู้ใช้กลุ่มและผู้ใช้รีโมท"; -"(Group)" = "(กลุ่ม)"; -"Adding User to Share failed" = "เพิ่มผู้ใช้ไปยังแชร์ล้มเหลว"; -"Permissions" = "สิทธิ์การเข้าใช้งาน"; -"Invited: %@" = "เชิญแล้ว: %@"; -"Created: %@" = "สร้างแล้ว: %@"; -"Allows the users you share with to re-share" = "อนุญาตให้ผู้ใช้ที่คุณแชร์ด้วยแขร์อีกครั้ง"; -"Allows the users you share with to edit your shared files, and to collaborate" = "อนุญาตให้ผู้ใช้ให้สามารถแก้ไขข้อมูลที่คุณแชร์"; -"Allows the users you share with to create new files and add them to the share" = "อนุญาตให้ผู้ใช้สามารถเพิ่มไฟล์ไปยังแชร์ของคุณ"; -"Allows uploading a new version of a shared file and replacing it" = "อนุญาตให้อัปโหลดไฟล์แชร์เวอร์ชันใหม่และแทนที่ไฟล์เดิม"; -"Allows the users you share with to delete shared files" = "อนุญาตให้ผู้ใช้สามารถลบไฟล์ที่คุณแชร์"; -"Setting permission failed" = "ตั้งค่าการอนุญาตล้มเหลว"; -"Shared with" = "แชร์ด้วย"; -"Remove Recipient failed" = "ลบผู้รับล้มเหลว"; -"Remove Recipient" = "ลบผู้รับ"; -"Create" = "สร้าง"; -"Change" = "เปลี่ยนแปลง"; -"Recipients can view or download contents." = "ผู้รับสามารถดูหรือดาวน์โหลดเนื้อหา"; -"Recipients can view, download, edit, delete and upload contents." = "ผู้รับสามารถดู ดาวน์โหลด แก้ไข ลบและอัปโหลดเนื้อหาได้"; -"Receive files from multiple recipients without revealing the contents of the folder." = "รับไฟล์จากผู้รับหลายรายโดยไม่ต้องเปิดเผยเนื้อหาของโฟลเดอร์"; -"Download / View" = "ดาวน์โหลดหรือดู"; -"Download / View / Upload" = "ดาวน์โหลด ดู หรืออัปโหลด"; -"Upload only (File Drop)" = "เฉพาะอัปโหลด (วางไฟล์)"; -"Creating public link failed" = "สร้างลิงก์สาธารณะล้มเหลว"; -"Create Public Link" = "สร้างลิงก์สาธารณะ"; -"Links" = "ลิงก์"; -"Link" = "ลิงก์"; -"Setting expiration date failed" = "ตั้งค่าวันหมดอายุล้มเหลว"; -"Expiration date" = "วันที่หมดอายุ"; -"Copy Public Link" = "คัดลอกลิงก์สาธารณะ"; -"Delete Public Link" = "ลบลิงก์สาธารณะ"; -"Deleting Public Link failed" = "ลบลิงก์สาธารณะล้มเหลว"; -"Deleting password failed" = "ลบรหัสผ่านล้มเหลว"; -"Setting password failed" = "การตั้งรหัสผ่านล้มเหลว"; -"Type to update password" = "กรอกเพื่ออัปเดตรหัสผ่าน"; -"Cannot change permission" = "ไม่สามารถเปลี่ยนการอนุญาต"; -"Before you can set the permission\n%@,\n you must enter a password." = "ก่อนที่คุณจะสามารถตั้งค่าการอนุญาต\n%@\nคุณต้องใส่รหัสผ่าน"; -"Password Protected" = "รหัสผ่านถูกป้องกันแล้ว"; -"Pending Federated Invites" = "คำเชิญแบบรวมที่รอดำเนินการ"; -"Pending Invites" = "กำลังรอคำเชิญ"; -"Shared with you" = "แชร์กับคุณ"; -"Shared with others" = "แชร์กับผู้อื่น"; -"Shares" = "แชร์"; -"Copy Private Link" = "คัดลอกลิงก์ส่วนตัว"; -"Only recipients can use this link. Use it as a permanent link to point to this resource" = "ผู้รับเท่านั้นที่สามารถใช้ลิงก์นี้ ใช้เป็นลิงก์ถาวรเพื่อชี้ไปยังแหล่งข้อมูลนี้"; -"Accept Share failed" = "ยอมรับการแชร์ล้มเหลว"; -"Decline Share failed" = "ปฏิเสธการแชร์ล้มเหลว"; -"Accept" = "ยอมรับ"; -"Decline" = "ลดลง"; -"Declined" = "ปฏิเสธ"; -"Decline Share" = "ปฏิเสธแชร์"; -"Unshare" = "ยกเลิกการแชร์"; -"Unshare failed" = "ยกเลิกการแชร์ไม่สำเร็จ"; -"Are you sure you want to unshare these items?" = "คุณแน่ใจว่าต้องการยกเลิกการแชร์รายการเหล่านี้?"; -"Are you sure you want to unshare this item?" = "คุณแน่ใจว่าต้องการยกเลิกการแชร์รายการนี้?"; -"Share" = "แชร์"; -"Read" = "อ่าน"; -"Can Share" = "สามารถแชร์"; -"Can Edit" = "สามารถแก้ไข"; -"Can Edit and Change" = "สามารถแก้ไขและเปลี่ยนแปลง"; -"Can Create" = "สามารถสร้าง"; -"Can Change" = "สามารถเปลี่ยน"; -"Can Delete" = "สามารถลบ"; -"Accept Invite %@" = "ยอมรับคำเชิญ %@"; -"Decline Invite %@" = "ปฏิเสธคำเชิญของ %@"; -"Decline cannot be undone." = "ไม่สามารถยกเลิกได้"; -"Sharing" = "แชร์ข้อมูล"; -"You" = "คุณ"; -"Share this file" = "แชร์ไฟล์นี้"; -"Share this folder" = "แชร์โฟลเดอร์นี้"; -"shared" = "ถูกแชร์"; -"Owner" = "เจ้าของ"; -"Private Link" = "ลิงก์ส่วนบุคคล"; +"Searching Shares…" = "Po kërkohet në Ndarje…"; +"Recipient" = "Marrës"; +"Recipients" = "Marrëss"; +"Public Link" = "Lidhje Publike"; +"Public Links" = "Lidhje Publike"; +"Shared by %@" = "Ndarë nga %@"; +"Invite Recipient" = "Ftoni Marrës"; +"Recipients" = "Marrëss"; +"Add email or name" = "Shtoni email ose emër"; +"Users" = "Përdorues"; +"Groups" = "Grupe"; +"Start typing to search users, groups and remote users." = "Filloni të shtypni që të kërkohet për përdorues, grupe dhe përdorues të largët."; +"(Group)" = "(Grup)"; +"Adding User to Share failed" = "Dështoi shtim Përdoruesi te Ndarje"; +"Permissions" = "Leje"; +"Invited: %@" = "U ftua: %@"; +"Created: %@" = "U krijua: %@"; +"Allows the users you share with to re-share" = "Lejon që përdoruesit me të cilët ndani të rindajnë"; +"Allows the users you share with to edit your shared files, and to collaborate" = "U lejon përdoruesve me të cilët ndani gjëra të përpunojnë kartelat tuaja që ndani, dhe të bashkëpunojnë"; +"Allows the users you share with to create new files and add them to the share" = "U lejon përdoruesve me të cilët ndani gjëra të krijojnë kartela të reja dhe t’i shtojnë ato te ndarja"; +"Allows uploading a new version of a shared file and replacing it" = "Lejon ngarkimin e një versioni të ri të një kartelë të ndarë, si dhe zëvendësim të saj"; +"Allows the users you share with to delete shared files" = "U lejon përdoruesve me të cilët ndani gjëra të fshijnë kartela të ndara"; +"Setting permission failed" = "Dështoi dhënia e lejes"; +"Shared with" = "Ndarë me:"; +"Remove Recipient failed" = "Dështoi Heqje Marrësi"; +"Remove Recipient" = "Hiqe Marrësin"; +"Create" = "Krijoje"; +"Change" = "Ndryshoje"; +"Recipients can view or download contents." = "Marrësit mund të shohin dhe shkarkojnë lëndë."; +"Recipients can view, download, edit, delete and upload contents." = "Marrësit mund të shohin shkarkojnë, përpunojnë, fshijnë dhe ngarkojnë lëndë."; +"Receive files from multiple recipients without revealing the contents of the folder." = "Merr kartela prej të tjerësh pa e zbuluar lëndën e dosjes."; +"Download / View" = "Të shkarkojë / Të shohë"; +"Download / View / Upload" = "Të shkarkojë / Të shohë / Të ngarkojë"; +"Upload only (File Drop)" = "Vetëm ngarkim (Lënie Kartele)"; +"Creating public link failed" = "Dështoi krijimi i lidhjes publike"; +"Create Public Link" = "Krijoni Lidhje Publike"; +"Links" = "Lidhje"; +"Link" = "Lidhje"; +"Setting expiration date failed" = "Dështoi caktimi i datës së skadimit"; +"Expiration date" = "Datë skadimi"; +"Copy Public Link" = "Kopjo Lidhje Publike"; +"Delete Public Link" = "Fshi Lidhje Publike"; +"Deleting Public Link failed" = "Dështoi fshirja e Lidhjes Publike"; +"Deleting password failed" = "Dështoi fshirja e fjalëkalimit"; +"Setting password failed" = "Dështoi caktimi i fjalëkalimit"; +"Type to update password" = "Shtypni që të përditësohet fjalëkalimi"; +"Cannot change permission" = "S’ndryshohet dot leje"; +"Before you can set the permission\n%@,\n you must enter a password." = "Përpara se të caktoni lejen\n%@,\n duhet të jepni një fjalëkalim."; +"Password Protected" = "Mbrojtur me Fjalëkalim"; +"Pending Federated Invites" = "Ftesa të Federuara Pezull"; +"Pending Invites" = "Ftesa Pezull"; +"Shared with you" = "Të ndara me ju"; +"Shared with others" = "Të ndara me të tjerët"; +"Shares" = "Ndarje"; +"Copy Private Link" = "Kopjo Lidhje Private"; +"Only recipients can use this link. Use it as a permanent link to point to this resource" = "Vetëm marrësit mund ta përdorin këtë lidhje. Përdoreni si një lidhje të përhershme që shpie te ky burim"; +"Accept Share failed" = "Dështoi Pranimi i Ndarjes"; +"Decline Share failed" = "Dështoi Hedhja Tej e Ndarjes"; +"Accept" = "Pranoje"; +"Decline" = "Hidhe poshtë"; +"Declined" = "Hedhur Poshtë"; +"Decline Share" = "Hidhe Poshtë Ndarjen"; +"Unshare" = "Hiqi ndarjen me të tjerë"; +"Unshare failed" = "Dështoi heqja e ndarjes"; +"Are you sure you want to unshare these items?" = "Jeni i sigurt se doni të hiqet ndarja e këtyre objekteve?"; +"Are you sure you want to unshare this item?" = "Jeni i sigurt se doni të hiqet ndarja e këtij objekti?"; +"Share" = "Ndaje me të tjerë"; +"Read" = "Lexoni"; +"Can Share" = "Mund të Ndajë"; +"Can Edit" = "Mund të Përpunojë"; +"Can Edit and Change" = "Mund të Përpunojë dhe Ndryshojë"; +"Can Create" = "Mund të Krijojë"; +"Can Change" = "Mund të Ndryshojë"; +"Can Delete" = "Mund të Fshijë"; +"Accept Invite %@" = "Pranoni Ftesën %@"; +"Decline Invite %@" = "Hidheni Tej Ftesën %@"; +"Decline cannot be undone." = "Hedhja tej s’mund të zhbëhet."; +"Sharing" = "Ndarje me të tjerët"; +"You" = "Ju"; +"Share this file" = "Ndajeni këtë kartelë"; +"Share this folder" = "Ndajeni këtë dosje"; +"shared" = "e ndarë me të tjerë"; +"Owner" = "I zoti"; +"Private Link" = "Lidhje Private"; /* Quick Access view */ -"Quick Access" = "เข้าถึงด่วน"; -"Collection" = "รวบรวม"; -"Recents" = "ล่าสุด"; -"Favorites"= "รายการโปรด"; -"Images" = "รูปภาพ"; -"PDF Documents" = "เอกสาร PDF"; +"Quick Access" = "Hyrje e Shpejtë"; +"Collection" = "Koleksion"; +"Recents" = "Tani së fundi"; +"Favorites"= "Të parapëlqyera"; +"Images" = "Foto"; +"PDF Documents" = "Dokumente PDF"; /* Media files settings */ -"Media Files" = "ไฟล์สื่อ"; -"Streaming Enabled" = "เปิดใช้งานการสตรีมแล้ว"; +"Media Files" = "Kartela Media"; +"Streaming Enabled" = "Transmetim i Aktivizuar"; /* Photo upload settings */ -"Media Upload" = "การอัปโหลดสื่อ"; -"Convert HEIC to JPEG" = "แปลง HEIC เป็น JPEG"; -"Convert videos to MP4" = "แปลงวิดีโอเป็น MP4"; +"Media Upload" = "Ngarkim Media"; +"Convert HEIC to JPEG" = "Shndërroje HEIC në JPEG"; +"Convert videos to MP4" = "Shndërroji videot në MP4"; +"Auto Upload Photos" = "Ngarkoni Foto Në Çast"; +"Auto Upload Videos" = "Ngarkoni Video Në Çast"; +"Account" = "Llogari"; +"Accounts" = "Llogari"; +"Select account" = "Përzgjidhni llogari"; +"Upload Path" = "Shteg Ngarkimi"; +"Select Upload Path" = "Përzgjidhni Shteg Ngarkimi"; +"Auto upload of media was disabled since configured account / folder was not found" = "Ngarkimi i menjëhershëm i medias qe çaktivizuar, ngaqë s’u gjet llogari / dosje e formësuar "; /* Progress summarizer */ -"Creating %ld folders…" = "กำลังสร้าง %ld โฟลเดอร์"; -"Moving %ld items…" = "กำลังย้าย %ld ไฟล์"; -"Copying %ld items…" = "กำลังคัดลอก %ld ไฟล์"; -"Deleting %ld items…" = "กำลังลบ %ld ไฟล์"; -"Uploading %ld files…" = "กำลังอัปโหลด %ld ไฟล์"; -"Downloading %ld files…" = "กำลังดาวน์โหลด %ld ไฟล์"; -"Updating %ld items…" = "กำลังอัปเดต %ld ไฟล์"; +"Creating %ld folders…" = "Po krijohen %ld dosje…"; +"Moving %ld items…" = "Po lëziven %ld objekte…"; +"Copying %ld items…" = "Po kopjohen %ld objekte…"; +"Deleting %ld items…" = "Po fshihen %ld objekte…"; +"Uploading %ld files…" = "Po ngarkohen %ld kartela…"; +"Downloading %ld files…" = "Po shkarkohen %ld kartela…"; +"Updating %ld items…" = "Po përditësohen %ld objekte…"; /* Offline storage management */ -"Free on %@" = "ฟรีบน %@"; -"unknown" = "ไม่รู้จัก"; -"Offline files use" = "ใช้ไฟล์ออฟไลน์"; -"Compacting of '%@' failed" = "กำลังย่อข้อมูลพบความล้มเหลว '%@'"; -"Include available offline files" = "รวมถึงไฟล์ออฟไลน์ที่มีอยู่"; -"Delete Local Copies" = "ลบสำเนาในเครื่อง"; -"Manage" = "การจัดการ"; -"Storage" = "พื้นที่จัดเก็บข้อมูล"; - -"Really include available offline files?" = "รวมไฟล์ออฟไลน์ที่มีอยู่?"; -"Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "ไฟล์และโฟลเดอร์ที่ทำเครื่องหมายว่าพร้อมใช้งานแบบออฟไลน์จะไม่สามารถใช้งานได้ ไฟล์เหล่านี้จะถูกดาวน์โหลดอีกในครั้งต่อไปที่คุณเข้าสู่บัญชีของคุณ (จำเป็นต้องเชื่อมต่อ)"; +"Free on %@" = "Falas në %@"; +"unknown" = "i panjohur"; +"Offline files use" = "Përdorim kartelash pa internet"; +"Compacting of '%@' failed" = "Dështoi ngjeshja e '%@'"; +"Include available offline files" = "Përfshi kartela të passhme jashtë linje"; +"Delete Local Copies" = "Fshi Kopje Vendore"; +"Manage" = "Administroni"; +"Storage" = "Depozitë"; +"Compacting" = "Po ngjeshet"; + +"Really include available offline files?" = "Të përfshihen vërtet kartela të passhme jashtë linje?"; +"Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "Kartelat dhe dosjet me shenjë për Të Passhme Jashtë Linje s’do të mund të përdoren dot. Ato do të rishkarkohen herës pasuese që bëni hyrjen në llogarinë tuaj (lypset lidhje në internet)."; /* Available offline */ -"Root folder" = "โฟลเดอร์รูท"; -"at" = "ที่"; -"(no match)" = "(ไม่ตรงกัน)"; +"Root folder" = "Dosje rrënjë"; +"at" = "në"; +"(no match)" = "(pa përputhje)"; -"Make available offline" = "ทำให้พร้อมใช้งานแบบออฟไลน์"; -"Make unavailable offline" = "ทำให้พร้อมใช้งานแบบออฟไลน์"; +"Make available offline" = "Bëje të passhme jo në linjë"; +"Make unavailable offline" = "Bëje jo të passhme jashtë linje"; -"Available Offline" = "พร้อมใช้งานแบบออฟไลน์แล้ว"; -"No items have been selected for offline availability." = "ไม่มีรายการที่ถูกเลือกสำหรับการใช้งานแบบออฟไลน์"; +"Available Offline" = "E passhme Jashtë Linje"; +"No items have been selected for offline availability." = "S’janë përzgjedhur objekte për t’i pasur jashtë linje."; -"Overview" = "ภาพรวม"; -"All Files" = "ไฟล์ทั้งหมด"; +"Overview" = "Përmbledhje"; +"All Files" = "Krejt Kartelat"; /* Import File */ -"Save File" = "บันทึกไฟล์"; -"Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "เลือกบัญชีและโฟลเดอร์ที่ต้องการนำเข้า\n\nสามารถนำเข้าได้ครั้งละหนึ่งไฟล์เท่านั้น"; +"Save File" = "Ruaje Kartelën"; +"Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Zgjidhni një llogari dhe dosje ku të importohet kartela.\n\nMund të importohet vetëm një kartelë në herë."; + +/* Key Commands */ +"Select Next" = "Përzgjidhni Pasuesen"; +"Select Previous" = "Përzgjidhni të Mëparshmen"; +"Open Selected" = "Hap të Përzgjedhurën"; +"Change Sort Order" = "Ndrysho Rend Renditjeje"; +"Search" = "Kërko"; +"Back" = "Mbrapsht"; +"Save" = "Ruaje"; +"Sort by %@" = "Renditi sipas %@"; +"Tab %@" = "Skeda %@"; +"Select Last Item on Page" = "Përzgjidhni Elementin e Fundit në Faqe"; +"Scroll to Top" = "Rrëshqitni drejt Kreut"; +"Scroll to Bottom" = "Rrëshqitni drejt Fundit"; +"Copy to Pasteboard" = "Kopjoje në të Papastër"; +"Import from Pasteboard" = "Importo prej të Papastre"; +"Next" = "Pasuesja"; +"Previous" = "E mëparshmja"; +"Favorite" = "Vëre Si të Parapëlqyer"; +"Cut" = "Prije"; diff --git a/ownCloud/Resources/zh-Hans.lproj/Localizable.strings b/ownCloud/Resources/zh-Hans.lproj/Localizable.strings index 41066f21c..b86a3074e 100644 Binary files a/ownCloud/Resources/zh-Hans.lproj/Localizable.strings and b/ownCloud/Resources/zh-Hans.lproj/Localizable.strings differ diff --git a/ownCloud/SceneDelegate.swift b/ownCloud/SceneDelegate.swift index b1eee745f..496b19230 100644 --- a/ownCloud/SceneDelegate.swift +++ b/ownCloud/SceneDelegate.swift @@ -21,14 +21,16 @@ import ownCloudSDK @available(iOS 13.0, *) class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? + var window: ThemeWindow? // UIWindowScene delegate func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let windowScene = scene as? UIWindowScene { - window = UIWindow(windowScene: windowScene) - let serverListTableViewController = ServerListTableViewController(style: UITableView.Style.plain) + window = ThemeWindow(windowScene: windowScene) + + let serverListTableViewController = ServerListTableViewController(style: .plain) serverListTableViewController.restorationIdentifier = "ServerListTableViewController" + let navigationController = ThemeNavigationController(rootViewController: serverListTableViewController) window?.rootViewController = navigationController window?.addSubview((navigationController.view)!) @@ -44,7 +46,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return scene.userActivity } - func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool { + @discardableResult func configure(window: ThemeWindow?, with activity: NSUserActivity) -> Bool { guard let bookmarkUUIDString = activity.userInfo?[ownCloudOpenAccountAccountUuidKey] as? String, let bookmarkUUID = UUID(uuidString: bookmarkUUIDString), let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID), let navigationController = window?.rootViewController as? ThemeNavigationController, let serverListController = navigationController.topViewController as? ServerListTableViewController else { return false } @@ -68,4 +70,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return false } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + if let urlContext = URLContexts.first { + ImportFilesController(url: urlContext.url, copyBeforeUsing: urlContext.options.openInPlace).accountUI() + } + } } diff --git a/ownCloud/Server List/ServerListTableHeaderView.swift b/ownCloud/Server List/ServerListTableHeaderView.swift index a31ae2b76..4cea3e891 100644 --- a/ownCloud/Server List/ServerListTableHeaderView.swift +++ b/ownCloud/Server List/ServerListTableHeaderView.swift @@ -57,7 +57,7 @@ class ServerListTableHeaderView: UIView, Themeable { textLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: textLabelTopMargin), textLabel.leftAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leftAnchor, constant: textLabelHorizontalMargin), - textLabel.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: textLabelHorizontalMargin), + textLabel.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -textLabelHorizontalMargin), textLabel.heightAnchor.constraint(equalToConstant: textLabelHeight) ]) diff --git a/ownCloud/Server List/ServerListTableViewController.swift b/ownCloud/Server List/ServerListTableViewController.swift index e90a359ae..581e69676 100644 --- a/ownCloud/Server List/ServerListTableViewController.swift +++ b/ownCloud/Server List/ServerListTableViewController.swift @@ -124,14 +124,10 @@ class ServerListTableViewController: UITableViewController, Themeable { updateNoServerMessageVisibility() - let helpBarButtonItem = UIBarButtonItem(title: "Feedback", style: UIBarButtonItem.Style.plain, target: self, action: #selector(help)) - helpBarButtonItem.accessibilityIdentifier = "helpBarButtonItem" - let settingsBarButtonItem = UIBarButtonItem(title: "Settings".localized, style: UIBarButtonItem.Style.plain, target: self, action: #selector(settings)) settingsBarButtonItem.accessibilityIdentifier = "settingsBarButtonItem" self.toolbarItems = [ - helpBarButtonItem, UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil), settingsBarButtonItem ] @@ -326,54 +322,63 @@ class ServerListTableViewController: UITableViewController, Themeable { } func delete(bookmark: OCBookmark, at indexPath: IndexPath) { - OCBookmarkManager.lock(bookmark: bookmark) + var presentationStyle: UIAlertController.Style = .actionSheet + if UIDevice.current.isIpad() { + presentationStyle = .alert + } - OCCoreManager.shared.scheduleOfflineOperation({ (bookmark, completionHandler) in - let vault : OCVault = OCVault(bookmark: bookmark) + let alertController = ThemedAlertController(title: NSString(format: "Really delete '%@'?".localized as NSString, bookmark.shortName) as String, + message: "This will also delete all locally stored file copies.".localized, + preferredStyle: presentationStyle) - vault.erase(completionHandler: { (_, error) in - OnMainThread { - if error != nil { - // Inform user if vault couldn't be erased - let alertController = ThemedAlertController(title: NSString(format: "Deletion of '%@' failed".localized as NSString, bookmark.shortName as NSString) as String, - message: error?.localizedDescription, - preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) - alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) + alertController.addAction(UIAlertAction(title: "Delete".localized, style: .destructive, handler: { (_) in - self.present(alertController, animated: true, completion: nil) - } else { - // Success! We can now remove the bookmark - self.ignoreServerListChanges = true + OCBookmarkManager.lock(bookmark: bookmark) - OCBookmarkManager.shared.removeBookmark(bookmark) + OCCoreManager.shared.scheduleOfflineOperation({ (bookmark, completionHandler) in + let vault : OCVault = OCVault(bookmark: bookmark) - self.tableView.performBatchUpdates({ - self.tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade) - }, completion: { (_) in - self.ignoreServerListChanges = false - }) + vault.erase(completionHandler: { (_, error) in + OnMainThread { + if error != nil { + // Inform user if vault couldn't be erased + let alertController = ThemedAlertController(title: NSString(format: "Deletion of '%@' failed".localized as NSString, bookmark.shortName as NSString) as String, + message: error?.localizedDescription, + preferredStyle: .alert) - self.updateNoServerMessageVisibility() - } + alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) - OCBookmarkManager.unlock(bookmark: bookmark) + self.present(alertController, animated: true, completion: nil) + } else { + // Success! We can now remove the bookmark + self.ignoreServerListChanges = true - completionHandler() - } - }) - }, for: bookmark) - } + OCBookmarkManager.shared.removeBookmark(bookmark) - var themeCounter : Int = 0 + self.tableView.performBatchUpdates({ + self.tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade) + }, completion: { (_) in + self.ignoreServerListChanges = false + }) - @IBAction func help() { - // Prevent any in-progress connection from being shown - resetPreviousBookmarkSelection() + self.updateNoServerMessageVisibility() + } + + OCBookmarkManager.unlock(bookmark: bookmark) + + completionHandler() + } + }) + }, for: bookmark) + })) - VendorServices.shared.sendFeedback(from: self) + self.present(alertController, animated: true, completion: nil) } + var themeCounter : Int = 0 + @IBAction func settings() { let viewController : SettingsViewController = SettingsViewController(style: .grouped) diff --git a/ownCloud/Settings/MediaUploadSettingsSection.swift b/ownCloud/Settings/MediaUploadSettingsSection.swift index 77b7306fb..d57c15854 100644 --- a/ownCloud/Settings/MediaUploadSettingsSection.swift +++ b/ownCloud/Settings/MediaUploadSettingsSection.swift @@ -51,7 +51,9 @@ extension UserDefaults { } get { - return self.bool(forKey: MediaUploadKeys.ConvertVideosToMP4Key.rawValue) + // TODO: MPEG-4 conversion is broken in iOS13, revisit it again in later releases + //return self.bool(forKey: MediaUploadKeys.ConvertVideosToMP4Key.rawValue) + return false } } @@ -169,7 +171,8 @@ class MediaUploadSettingsSection: SettingsSection { }, title: "Convert videos to MP4".localized, value: self.userDefaults.convertVideosToMP4, identifier: "convert_to_mp4") self.add(row: convertPhotosSwitchRow!) - self.add(row: convertVideosSwitchRow!) + // TODO: MPEG-4 conversion is broken in iOS13, revisit it again in later releases + //self.add(row: convertVideosSwitchRow!) // Instant upload requires at least one configured account if OCBookmarkManager.shared.bookmarks.count > 0 { @@ -187,7 +190,7 @@ class MediaUploadSettingsSection: SettingsSection { }) } - }, title: "Instant Upload Photos".localized, value: self.userDefaults.instantUploadPhotos) + }, title: "Auto Upload Photos".localized, value: self.userDefaults.instantUploadPhotos) instantUploadVideosRow = StaticTableViewRow(switchWithAction: { [weak self] (_, sender) in if let convertSwitch = sender as? UISwitch { @@ -202,7 +205,7 @@ class MediaUploadSettingsSection: SettingsSection { } }) } - }, title: "Instant Upload Videos".localized, value: self.userDefaults.instantUploadVideos) + }, title: "Auto Upload Videos".localized, value: self.userDefaults.instantUploadVideos) bookmarkAndPathSelectionRow = StaticTableViewRow(valueRowWithAction: { [weak self] (_, _) in self?.showAccountSelectionViewController() @@ -252,8 +255,8 @@ class MediaUploadSettingsSection: SettingsSection { } else { self.userDefaults.resetInstantUploadConfiguration() OnMainThread { - let alertController = ThemedAlertController(with: "Instant upload disabled".localized, - message: "Instant upload of media was disabled since configured account / folder was not found".localized) + let alertController = ThemedAlertController(with: "Auto upload disabled".localized, + message: "Auto upload of media was disabled since configured account / folder was not found".localized) self.viewController?.present(alertController, animated: true, completion: nil) } } diff --git a/ownCloud/Settings/Passcode/AppLockManager.swift b/ownCloud/Settings/Passcode/AppLockManager.swift index 4e14575e3..c225a5a9e 100644 --- a/ownCloud/Settings/Passcode/AppLockManager.swift +++ b/ownCloud/Settings/Passcode/AppLockManager.swift @@ -23,9 +23,6 @@ import LocalAuthentication class AppLockManager: NSObject { // MARK: - UI - private var window: AppLockWindow? - private var passcodeViewController: PasscodeViewController? - private var userDefaults: UserDefaults // MARK: - State @@ -137,65 +134,122 @@ class AppLockManager: NSObject { NotificationCenter.default.addObserver(self, selector: #selector(self.appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.updateLockscreens), name: ThemeWindow.themeWindowListChangedNotification, object: nil) } deinit { NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: ThemeWindow.themeWindowListChangedNotification, object: nil) } // MARK: - Show / Dismiss Passcode View func showLockscreenIfNeeded(forceShow: Bool = false, context: LAContext = LAContext()) { if self.shouldDisplayLockscreen || forceShow { - if passcodeViewController == nil { - passcodeViewController = PasscodeViewController(completionHandler: { (_, passcode: String) in - self.attemptUnlock(with: passcode) - }) - - passcodeViewController?.message = "Enter code".localized - passcodeViewController?.cancelButtonHidden = false - - passcodeViewController?.screenBlurringEnabled = forceShow && !self.shouldDisplayLockscreen - - window = AppLockWindow(frame: UIScreen.main.bounds) - /* - Workaround to the lack of status bar animation when returning true for prefersStatusBarHidden in - PasscodeViewController. - - The documentation notes that "The ordering of windows within a given window level is not guaranteed.", - so that with a future iOS update this might break and the status bar be displayed regardless. In that - case, implement prefersStatusBarHidden in PasscodeViewController to return true and remove the dismiss - animation (the re-appearance of the status bar will lead to a jump in the UI otherwise). - */ - window?.windowLevel = UIWindow.Level.statusBar - window?.rootViewController = passcodeViewController! - window?.makeKeyAndVisible() - - self.startLockCountdown() - } else { - passcodeViewController?.screenBlurringEnabled = forceShow - } + lockscreenOpenForced = forceShow + lockscreenOpen = true - // Show biometrical - if !forceShow, !self.shouldDisplayCountdown { - showBiometricalAuthenticationInterface(context: context) - } + // Show biometrical + if !forceShow, !self.shouldDisplayCountdown { + showBiometricalAuthenticationInterface(context: context) + } } } func dismissLockscreen(animated:Bool) { - let hideWindow = { - self.window?.isHidden = true - self.passcodeViewController = nil - self.window = nil + if animated { + let animationGroup = DispatchGroup() + + for themeWindow in ThemeWindow.themeWindows { + if let appLockWindow = applockWindowByWindow.object(forKey: themeWindow) { + animationGroup.enter() + + appLockWindow.hideWindowAnimation { + appLockWindow.isHidden = true + animationGroup.leave() + } + } + } + + animationGroup.notify(queue: .main) { + self.lockscreenOpen = false + } + } else { + self.lockscreenOpen = false } + } - if animated { - self.window?.hideWindowAnimation { - hideWindow() + // MARK: - Lock window management + private var lockscreenOpenForced : Bool = false + private var lockscreenOpen : Bool = false { + didSet { + updateLockscreens() + } + } + + private var passcodeControllerByWindow : NSMapTable = NSMapTable.weakToStrongObjects() + private var applockWindowByWindow : NSMapTable = NSMapTable.weakToStrongObjects() + + @objc func updateLockscreens() { + if lockscreenOpen { + for themeWindow in ThemeWindow.themeWindows { + if let passcodeViewController = passcodeControllerByWindow.object(forKey: themeWindow) { + passcodeViewController.screenBlurringEnabled = lockscreenOpenForced + } else { + var appLockWindow : AppLockWindow + var passcodeViewController : PasscodeViewController + + passcodeViewController = PasscodeViewController(completionHandler: { (viewController: PasscodeViewController, passcode: String) in + self.attemptUnlock(with: passcode, passcodeViewController: viewController) + }) + + passcodeViewController.message = "Enter code".localized + passcodeViewController.cancelButtonHidden = false + + passcodeViewController.screenBlurringEnabled = lockscreenOpenForced && !self.shouldDisplayLockscreen + + if #available(iOS 13, *) { + if let windowScene = themeWindow.windowScene { + appLockWindow = AppLockWindow(windowScene: windowScene) + } else { + appLockWindow = AppLockWindow(frame: UIScreen.main.bounds) + } + } else { + appLockWindow = AppLockWindow(frame: UIScreen.main.bounds) + } + /* + Workaround to the lack of status bar animation when returning true for prefersStatusBarHidden in + PasscodeViewController. + + The documentation notes that "The ordering of windows within a given window level is not guaranteed.", + so that with a future iOS update this might break and the status bar be displayed regardless. In that + case, implement prefersStatusBarHidden in PasscodeViewController to return true and remove the dismiss + animation (the re-appearance of the status bar will lead to a jump in the UI otherwise). + */ + appLockWindow.windowLevel = UIWindow.Level.statusBar + appLockWindow.rootViewController = passcodeViewController + appLockWindow.makeKeyAndVisible() + + passcodeControllerByWindow.setObject(passcodeViewController, forKey: themeWindow) + applockWindowByWindow.setObject(appLockWindow, forKey: themeWindow) + + self.startLockCountdown() + + if self.shouldDisplayCountdown { + passcodeViewController.keypadButtonsHidden = true + updateLockCountdown() + } + } } } else { - hideWindow() + for themeWindow in ThemeWindow.themeWindows { + if let appLockWindow = applockWindowByWindow.object(forKey: themeWindow) { + appLockWindow.isHidden = true + + passcodeControllerByWindow.removeObject(forKey: themeWindow) + applockWindowByWindow.removeObject(forKey: themeWindow) + } + } } } @@ -215,7 +269,7 @@ class AppLockManager: NSObject { } // MARK: - Unlock - func attemptUnlock(with testPasscode: String?, customErrorMessage: String? = nil) { + func attemptUnlock(with testPasscode: String?, customErrorMessage: String? = nil, passcodeViewController: PasscodeViewController? = nil) { if testPasscode == self.passcode { unlocked = true lastApplicationBackgroundedDate = nil @@ -266,7 +320,9 @@ class AppLockManager: NSObject { // MARK: - Countdown display private func startLockCountdown() { if self.shouldDisplayCountdown { - passcodeViewController?.keypadButtonsHidden = true + performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.keypadButtonsHidden = true + } updateLockCountdown() lockTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateLockCountdown), userInfo: nil, repeats: true) @@ -288,41 +344,64 @@ class AppLockManager: NSObject { } let timeoutMessage:String = NSString(format: "Please try again in %@".localized as NSString, dateFormatted!) as String - self.passcodeViewController?.timeoutMessage = timeoutMessage + + performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.timeoutMessage = timeoutMessage + } if date <= Date() { // Time elapsed, allow entering passcode again self.lockTimer?.invalidate() - self.passcodeViewController?.keypadButtonsHidden = false - self.passcodeViewController?.timeoutMessage = nil - self.passcodeViewController?.errorMessage = nil + performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.keypadButtonsHidden = false + passcodeViewController.timeoutMessage = nil + passcodeViewController.errorMessage = nil + } } } } - // MARK: - Biometrical Unlock - func showBiometricalAuthenticationInterface(context: LAContext) { + private func performPasscodeViewControllerUpdates(_ updateHandler: (_: PasscodeViewController) -> Void) { + for themeWindow in ThemeWindow.themeWindows { + if let passcodeViewController = passcodeControllerByWindow.object(forKey: themeWindow) { + updateHandler(passcodeViewController) + } + } + } - if shouldDisplayLockscreen, biometricalSecurityEnabled { + // MARK: - Biometrical Unlock + private var biometricalAuthenticationInterfaceShown : Bool = false + func showBiometricalAuthenticationInterface(context: LAContext) { + if shouldDisplayLockscreen, biometricalSecurityEnabled, !biometricalAuthenticationInterfaceShown { var evaluationError: NSError? // Check if the device can evaluate the policy. if context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &evaluationError) { let reason = NSString.init(format: "Unlock %@".localized as NSString, OCAppIdentity.shared.appName!) as String - self.passcodeViewController?.errorMessage = nil + performPasscodeViewControllerUpdates { (passcodeViewController) in + OnMainThread { + passcodeViewController.errorMessage = nil + } + } context.localizedCancelTitle = "Enter code".localized context.localizedFallbackTitle = "" + self.biometricalAuthenticationInterfaceShown = true + context.evaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { (success, error) in + self.biometricalAuthenticationInterfaceShown = false + if success { - //Fill the passcode dots + // Fill the passcode dots OnMainThread { - self.passcodeViewController?.passcode = self.passcode + self.performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.passcode = self.passcode + } } - //Remove the passcode after small delay to give user feedback after use the biometrical unlock + // Remove the passcode after small delay to give user feedback after use the biometrical unlock OnMainThread(after: 0.3) { self.attemptUnlock(with: self.passcode) } @@ -331,7 +410,9 @@ class AppLockManager: NSObject { switch error { case LAError.biometryLockout: OnMainThread { - self.passcodeViewController?.errorMessage = error.localizedDescription + self.performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.errorMessage = error.localizedDescription + } } case LAError.authenticationFailed: @@ -345,9 +426,12 @@ class AppLockManager: NSObject { } } } else { - if let error = evaluationError, - self.biometricalSecurityEnabled { - self.passcodeViewController?.errorMessage = error.localizedDescription + if let error = evaluationError, biometricalSecurityEnabled { + OnMainThread { + self.performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.errorMessage = error.localizedDescription + } + } } } } diff --git a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift index 16f2fc4b8..e92ce158a 100644 --- a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift +++ b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift @@ -37,120 +37,66 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { guard userDefaults.instantUploadPhotos == true || userDefaults.instantUploadVideos == true else { return } - guard let bookmarkUUID = userDefaults.instantUploadBookmarkUUID else { return } + guard let bookmarkUUID = userDefaults.instantUploadBookmarkUUID else { + Log.warning(tagged: ["INSTANT_MEDIA_UPLOAD"], "Instant media upload enabled, but bookmark not configured") + return + } - guard let path = userDefaults.instantUploadPath else { return } + guard let path = userDefaults.instantUploadPath else { + Log.warning(tagged: ["INSTANT_MEDIA_UPLOAD"], "Instant media upload enabled, but path not configured") + return + } if let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { - - OCCoreManager.shared.requestCore(for: bookmark, setup:nil, completionHandler: {(core, coreError) in - if core != nil { - - func finalize() { - OCCoreManager.shared.returnCore(for: bookmark, completionHandler: { - self.completed() - }) - } - - core?.fetchUpdates(completionHandler: { (fetchError, _) in - if fetchError == nil { - self.uploadDirectoryTracking = core?.trackItem(atPath: path, trackingHandler: { (error, item, isInitial) in - - if isInitial { - if error != nil { - Log.error("Error \(String(describing: error))") - } - - if item != nil { - self.uploadMediaAssets(with: core, at: item!, completion: { - finalize() - }) - } else { - Log.warning("Instant upload directory not found") - userDefaults.resetInstantUploadConfiguration() - finalize() - self.showFeatureDisabledAlert() - } - } else { - self.uploadDirectoryTracking = nil - } - }) - } else { - Log.error("Fetching bookmark update failed with \(String(describing: fetchError))") - finalize() - } - }) - } else { - if coreError != nil { - Log.error("No core returned with error \(String(describing: coreError))") - self.result = .failure(coreError!) - } - self.completed() - } - }) + uploadMediaAssets(for: bookmark, at: path) } } - private func uploadMediaAssets(with core:OCCore?, at item:OCItem, completion:@escaping () -> Void) { + private func uploadMediaAssets(for bookmark:OCBookmark, at path:String) { guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } - var assets = [PHAsset]() + var photoAssets = [PHAsset]() + + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching images created after \(String(describing: userDefaults.instantUploadPhotosAfter))") // Add photo assets if let uploadPhotosAfter = userDefaults.instantUploadPhotosAfter { let fetchResult = self.fetchAssetsFromCameraRoll(.images, createdAfter: uploadPhotosAfter) if fetchResult != nil { fetchResult!.enumerateObjects({ (asset, _, _) in - assets.append(asset) + photoAssets.append(asset) }) } } + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Importing \(photoAssets.count) photo assets") + + if photoAssets.count > 0 { + MediaUploadQueue.shared.addUploads(Array(photoAssets), for: bookmark, at: path) + userDefaults.instantUploadPhotosAfter = photoAssets.last?.modificationDate + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Last added photo asset modification date: \(String(describing: userDefaults.instantUploadPhotosAfter))") + } + + var videoAssets = [PHAsset]() + + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching videos created after \(String(describing: userDefaults.instantUploadVideosAfter))") + // Add video assets if let uploadVideosAfter = userDefaults.instantUploadVideosAfter { let fetchResult = self.fetchAssetsFromCameraRoll(.videos, createdAfter: uploadVideosAfter) if fetchResult != nil { fetchResult!.enumerateObjects({ (asset, _, _) in - assets.append(asset) + videoAssets.append(asset) }) } } - // Perform actual upload operation - if assets.count > 0 { - self.upload(assets: assets, with: core, at: item, completion: { () in - OnMainThread { - completion() - } - }) - } else { - OnMainThread { - completion() - } - } - } - - private func upload(assets:[PHAsset], with core:OCCore?, at rootItem:OCItem, completion:@escaping () -> Void) { - - guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Importing \(videoAssets.count) video assets") - if assets.count > 0 { - Log.debug("Uploading \(assets.count) assets") - MediaUploadQueue.shared.uploadAssets(assets, with: core, at: rootItem, assetUploadCompletion: { (asset, finished) in - if let asset = asset { - switch asset.mediaType { - case .image: - userDefaults.instantUploadPhotosAfter = asset.modificationDate - case .video: - userDefaults.instantUploadVideosAfter = asset.modificationDate - default: - break - } - } - if finished { - completion() - } - }) + if videoAssets.count > 0 { + MediaUploadQueue.shared.addUploads(videoAssets, for: bookmark, at: path) + userDefaults.instantUploadVideosAfter = videoAssets.last?.modificationDate + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Last added video asset modification date: \(String(describing: userDefaults.instantUploadPhotosAfter))") } } @@ -191,6 +137,8 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { let sort = NSSortDescriptor(key: "modificationDate", ascending: true) fetchOptions.sortDescriptors = [sort] + + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching assets with options \(fetchOptions.debugDescription)") return PHAsset.fetchAssets(in: cameraRoll, options: fetchOptions) } @@ -200,9 +148,9 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { private func showFeatureDisabledAlert() { OnMainThread { - let alertController = ThemedAlertController(with: "Instant upload disabled".localized, - message: "Instant upload of media was disabled since configured account / folder was not found".localized) - UIApplication.shared.delegate?.window??.rootViewController?.present(alertController, animated: true, completion: nil) + let alertController = ThemedAlertController(with: "Auto upload disabled".localized, + message: "Auto upload of media was disabled since configured account / folder was not found".localized) + UIApplication.shared.currentWindow()?.rootViewController?.present(alertController, animated: true, completion: nil) } } } diff --git a/ownCloud/Tasks/PendingMediaUploadTaskExtension.swift b/ownCloud/Tasks/PendingMediaUploadTaskExtension.swift new file mode 100644 index 000000000..30cedcbed --- /dev/null +++ b/ownCloud/Tasks/PendingMediaUploadTaskExtension.swift @@ -0,0 +1,48 @@ +// +// PendingMediaUploadTaskExtension.swift +// ownCloud +// +// Created by Michael Neuwert on 09.10.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import Foundation +import ownCloudSDK +import Photos + +class PendingMediaUploadTaskExtension : ScheduledTaskAction { + + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.pending_media_upload") } + override class var locations : [OCExtensionLocationIdentifier]? { return [.appDidComeToForeground] } + override class var features : [String : Any]? { return [ FeatureKeys.runOnWifi : true] } + + private var uploadDirectoryTracking: OCCoreItemTracking? + private weak var weakCore: OCCore? + private var coreUpdatesHandler: OCCoreItemListFetchUpdatesCompletionHandler? + + override func run(background:Bool) { + + Log.debug(tagged: ["REMAINING_MEDIA_UPLOAD"], "Preparing...") + + // Do we have a selected bookmark? + guard let bookmark = OCBookmarkManager.lastBookmarkSelectedForConnection else { + Log.debug(tagged: ["REMAINING_MEDIA_UPLOAD"], "No bookmark selected...") + self.completed() + return + } + + MediaUploadQueue.shared.setNeedsScheduling(in: bookmark) + + self.completed() + } +} diff --git a/ownCloud/Tasks/ScheduledTaskManager.swift b/ownCloud/Tasks/ScheduledTaskManager.swift index 2d69f29c6..d8f954706 100644 --- a/ownCloud/Tasks/ScheduledTaskManager.swift +++ b/ownCloud/Tasks/ScheduledTaskManager.swift @@ -43,41 +43,44 @@ class ScheduledTaskManager : NSObject { static let shared = ScheduledTaskManager() private var state: State = .launched { - willSet { - if state != newValue { + + didSet { + if state != oldValue && state != .backgroundFetch { scheduleTasks() } } } + private static let lowBatteryThreshold : Float = 0.2 private var lowBatteryDetected = false { - willSet { - if self.lowBatteryDetected != newValue { + + didSet { + if self.lowBatteryDetected != oldValue { scheduleTasks() } } } private var externalPowerConnected = false { - willSet { - if self.externalPowerConnected != newValue { + didSet { + if self.externalPowerConnected != oldValue { scheduleTasks() } } } private var wifiDetected = false { - willSet { - if self.wifiDetected != newValue { + didSet { + if self.wifiDetected != oldValue { scheduleTasks() } } } private var photoLibraryChangeDetected = false { - willSet { - if self.photoLibraryChangeDetected != newValue && newValue == true { + didSet { + if self.photoLibraryChangeDetected != oldValue && self.photoLibraryChangeDetected == true { scheduleTasks() } } @@ -198,65 +201,63 @@ class ScheduledTaskManager : NSObject { } private func scheduleTasks(fetchCompletion:((UIBackgroundFetchResult) -> Void)? = nil, completion:((_ scheduledTaskCount:Int) -> Void)? = nil) { - OnMainThread { - - let state = self.state - let context = self.getCurrentContext() - - // Find a task to run - if let matches = try? OCExtensionManager.shared.provideExtensions(for: context) { - var bgFetchedNewDataTasks = 0 - var bgFailedTasks = 0 - let bgFetchGroup = DispatchGroup() - let queue = DispatchQueue.global(qos: .background) - - for match in matches { - if let task = match.extension.provideObject(for: context) as? ScheduledTaskAction { - // Set completion handler for the task performing background fetch - if state == .backgroundFetch { - task.backgroundFetchCompletion = { result in - switch result { - case .newData : - bgFetchedNewDataTasks += 1 - case .failed: - bgFailedTasks += 1 - default: - break - } - bgFetchGroup.leave() - } - } - let backgroundExecution = state == .background - if backgroundExecution { - task.runUntil = Date().addingTimeInterval(UIApplication.shared.backgroundTimeRemaining) - } - if state == .backgroundFetch { - bgFetchGroup.enter() - } - queue.async { - task.run(background: backgroundExecution) + let state = self.state + let context = self.getCurrentContext() + + // Find a task to run + if let matches = try? OCExtensionManager.shared.provideExtensions(for: context) { + var bgFetchedNewDataTasks = 0 + var bgFailedTasks = 0 + let bgFetchGroup = DispatchGroup() + let queue = DispatchQueue.global(qos: .background) + + for match in matches { + if let task = match.extension.provideObject(for: context) as? ScheduledTaskAction { + // Set completion handler for the task performing background fetch + if state == .backgroundFetch { + task.backgroundFetchCompletion = { result in + switch result { + case .newData : + bgFetchedNewDataTasks += 1 + case .failed: + bgFailedTasks += 1 + default: + break + } + bgFetchGroup.leave() } } - } - // Report background fetch result back to the OS - if state == .backgroundFetch { - bgFetchGroup.notify(queue: queue, execute: { - if bgFetchedNewDataTasks > 0 { - fetchCompletion?(.newData) - } else if bgFailedTasks > 0 { - fetchCompletion?(.failed) - } else { - fetchCompletion?(.noData) - } - }) + let backgroundExecution = state == .background + if backgroundExecution { + task.runUntil = Date().addingTimeInterval(UIApplication.shared.backgroundTimeRemaining) + } + if state == .backgroundFetch { + bgFetchGroup.enter() + } + queue.async { + task.run(background: backgroundExecution) + } } + } - completion?(matches.count) - } else { - completion?(0) + // Report background fetch result back to the OS + if state == .backgroundFetch { + bgFetchGroup.notify(queue: queue, execute: { + if bgFetchedNewDataTasks > 0 { + fetchCompletion?(.newData) + } else if bgFailedTasks > 0 { + fetchCompletion?(.failed) + } else { + fetchCompletion?(.noData) + } + }) } + + completion?(matches.count) + } else { + completion?(0) } } diff --git a/ownCloud/Theming/ThemeStyle+Extensions.swift b/ownCloud/Theming/ThemeStyle+Extensions.swift index ce291e7f1..1a866d690 100644 --- a/ownCloud/Theming/ThemeStyle+Extensions.swift +++ b/ownCloud/Theming/ThemeStyle+Extensions.swift @@ -92,11 +92,7 @@ extension ThemeStyle { @available(iOS 13.0, *) static func userInterfaceStyle() -> UIUserInterfaceStyle? { - if let themeWindow = (UIApplication.shared.delegate as? AppDelegate)?.window { - return themeWindow.traitCollection.userInterfaceStyle - } - - return nil + return UITraitCollection.current.userInterfaceStyle } static func considerAppearanceUpdate(animated: Bool = false) { diff --git a/ownCloud/Theming/UI/ThemeWindow.swift b/ownCloud/Theming/UI/ThemeWindow.swift index 8d8647dac..f2f0c88ed 100644 --- a/ownCloud/Theming/UI/ThemeWindow.swift +++ b/ownCloud/Theming/UI/ThemeWindow.swift @@ -19,6 +19,57 @@ import UIKit class ThemeWindow : UIWindow { + // MARK: - Theme window list + static let themeWindowListChangedNotification: NSNotification.Name = NSNotification.Name(rawValue: "ThemeWindowListChanged") + static private let _themeWindows : NSHashTable = NSHashTable.weakObjects() + + private static func addThemeWindow(_ window: ThemeWindow) { + OCSynchronized(self) { + _themeWindows.add(window) + } + NotificationCenter.default.post(name: ThemeWindow.themeWindowListChangedNotification, object: nil, userInfo: nil) + } + + private static func removeThemeWindow(_ window: ThemeWindow) { + OCSynchronized(self) { + _themeWindows.remove(window) + } + NotificationCenter.default.post(name: ThemeWindow.themeWindowListChangedNotification, object: nil, userInfo: nil) + } + + static var themeWindows : [ThemeWindow] { + var themeWindows : [ThemeWindow] = [] + + OCSynchronized(self) { + themeWindows = _themeWindows.allObjects + } + + return themeWindows + } + + // MARK: - Lifecycle + override init(frame: CGRect) { + super.init(frame: frame) + + ThemeWindow.addThemeWindow(self) + } + + @available(iOS 13, *) + override init(windowScene: UIWindowScene) { + super.init(windowScene: windowScene) + + ThemeWindow.addThemeWindow(self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + ThemeWindow.removeThemeWindow(self) + } + + // MARK: - Theme change detection override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) diff --git a/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift b/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift index ac629fb30..051c86124 100644 --- a/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift +++ b/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift @@ -70,7 +70,7 @@ final class CardPresentationController: UIPresentationController, Themeable { } private var windowFrame: CGRect { - if let window = UIApplication.shared.delegate?.window as? UIWindow { + if let window = UIApplication.shared.currentWindow() as? UIWindow { return window.bounds } else { return UIScreen.main.bounds diff --git a/ownCloud/UIKit Extensions/UIApplication+Extension.swift b/ownCloud/UIKit Extensions/UIApplication+Extension.swift new file mode 100644 index 000000000..e95e906f7 --- /dev/null +++ b/ownCloud/UIKit Extensions/UIApplication+Extension.swift @@ -0,0 +1,26 @@ +// +// UIApplication+Extension.swift +// ownCloud +// +// Created by Matthias Hühne on 11.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit + +public extension UIApplication { + + func currentWindow() -> UIWindow? { + return windows.first + } +} diff --git a/ownCloud/UIKit Extensions/UIViewController+Extension.swift b/ownCloud/UIKit Extensions/UIViewController+Extension.swift index ed9fce2ff..0650777a0 100644 --- a/ownCloud/UIKit Extensions/UIViewController+Extension.swift +++ b/ownCloud/UIKit Extensions/UIViewController+Extension.swift @@ -34,4 +34,21 @@ extension UIViewController { tabBarController.toolbar?.setItems(nil, animated: true) } } + + var topMostViewController: UIViewController { + + if let presented = self.presentedViewController, presented.isBeingDismissed == false { + return presented.topMostViewController + } + + if let navigation = self as? UINavigationController { + return navigation.visibleViewController?.topMostViewController ?? navigation + } + + if let tab = self as? UITabBarController { + return tab.selectedViewController?.topMostViewController ?? tab + } + + return self + } } diff --git a/ownCloudAppFramework/Resources/Info.plist b/ownCloudAppFramework/Resources/Info.plist index f6d9c8d36..674373422 100644 --- a/ownCloudAppFramework/Resources/Info.plist +++ b/ownCloudAppFramework/Resources/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 144 + 149 diff --git a/ownCloud/Client/SortMethod.swift b/ownCloudAppShared/Client/SortMethod.swift similarity index 93% rename from ownCloud/Client/SortMethod.swift rename to ownCloudAppShared/Client/SortMethod.swift index 742b2f83f..71fa56490 100644 --- a/ownCloud/Client/SortMethod.swift +++ b/ownCloudAppShared/Client/SortMethod.swift @@ -19,7 +19,7 @@ import Foundation import ownCloudSDK -typealias OCSort = Comparator +public typealias OCSort = Comparator public enum SortDirection: Int { case ascendant = 0 @@ -34,9 +34,9 @@ public enum SortMethod: Int { case date = 3 case shared = 4 - static var all: [SortMethod] = [alphabetically, type, size, date, shared] + public static var all: [SortMethod] = [alphabetically, type, size, date, shared] - func localizedName() -> String { + public func localizedName() -> String { var name = "" switch self { @@ -55,8 +55,8 @@ public enum SortMethod: Int { return name } - func comparator(direction: SortDirection) -> OCSort { - var comparator : OCSort + public func comparator(direction: SortDirection) -> OCSort { + var comparator: OCSort var combinedComparator: OCSort? let alphabeticComparator : OCSort = { (left, right) in diff --git a/ownCloudAppShared/Info.plist b/ownCloudAppShared/Info.plist new file mode 100644 index 000000000..9bcb24442 --- /dev/null +++ b/ownCloudAppShared/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition new file mode 100644 index 000000000..a7c27a30a --- /dev/null +++ b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition @@ -0,0 +1,2761 @@ + + + + + INEnums + + + INEnumDisplayName + Sorting Type + INEnumDisplayNameID + CwmgYp + INEnumGeneratesHeader + + INEnumName + SortingType + INEnumType + Regular + INEnumValues + + + INEnumValueDisplayName + unknown + INEnumValueDisplayNameID + 0w7gOP + INEnumValueName + unknown + + + INEnumValueDisplayName + Name + INEnumValueDisplayNameID + 8CnkN3 + INEnumValueIndex + 1 + INEnumValueName + name + + + INEnumValueDisplayName + Type + INEnumValueDisplayNameID + vY7pxV + INEnumValueIndex + 2 + INEnumValueName + type + + + INEnumValueDisplayName + Size + INEnumValueDisplayNameID + eZdCum + INEnumValueIndex + 3 + INEnumValueName + size + + + INEnumValueDisplayName + Date + INEnumValueDisplayNameID + lsb3bf + INEnumValueIndex + 4 + INEnumValueName + date + + + INEnumValueDisplayName + Shared + INEnumValueDisplayNameID + E5BAmD + INEnumValueIndex + 5 + INEnumValueName + shared + + + + + INEnumDisplayName + Sorting Direction + INEnumDisplayNameID + nZsCMf + INEnumGeneratesHeader + + INEnumName + SortingDirection + INEnumType + Regular + INEnumValues + + + INEnumValueDisplayName + unknown + INEnumValueDisplayNameID + JRUplN + INEnumValueName + unknown + + + INEnumValueDisplayName + Ascending + INEnumValueDisplayNameID + tHgDsi + INEnumValueIndex + 1 + INEnumValueName + ascending + + + INEnumValueDisplayName + Descending + INEnumValueDisplayNameID + ZB0VB7 + INEnumValueIndex + 2 + INEnumValueName + descending + + + + + INIntentDefinitionModelVersion + 1.1 + INIntentDefinitionNamespace + K5U8aR + INIntentDefinitionSystemVersion + 19B88 + INIntentDefinitionToolsBuildVersion + 11B53 + INIntentDefinitionToolsVersion + 11.2.1 + INIntents + + + INIntentCategory + request + INIntentConfigurable + + INIntentDescription + Get a list of all configured accounts + INIntentDescriptionID + wK4bqS + INIntentIneligibleForSuggestions + + INIntentManagedParameterCombinations + + + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get Account List + INIntentParameterCombinationTitleID + j7guHG + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetAccounts + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeFormatString + Here is your account list + INIntentResponseCodeFormatStringID + gxBJkl + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeFormatString + Here is your account list + INIntentResponseCodeFormatStringID + yrLv2M + INIntentResponseCodeName + successList + INIntentResponseCodeSuccess + + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + DSSHvU + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + iDEwxe + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 74Rz1p + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + xzMJ1y + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + y8oko2 + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + VSQYEZ + INIntentResponseCodeName + unlicensed + + + INIntentResponseLastParameterTag + 6 + INIntentResponseOutput + accountList + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Account List + INIntentResponseParameterDisplayNameID + Ul4Kf0 + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + accountList + INIntentResponseParameterObjectType + Account + INIntentResponseParameterObjectTypeNamespace + K5U8aR + INIntentResponseParameterSupportsMultipleValues + + INIntentResponseParameterTag + 5 + INIntentResponseParameterType + Object + + + + INIntentTitle + Get Accounts + INIntentTitleID + XKCjDe + INIntentType + Custom + INIntentVerb + Request + + + INIntentCategory + request + INIntentConfigurable + + INIntentDescription + Get a directory listing for the given account and path + INIntentDescriptionID + 2IChwb + INIntentIneligibleForSuggestions + + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 10 + INIntentManagedParameterCombinations + + path,account,sortType,sortDirection + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get Directory Listing for ${path} from ${account} + INIntentParameterCombinationTitleID + axuc4N + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetDirectoryListing + INIntentParameters + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + u8abus + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + INIntentParameterMetadataDefaultValue + / + INIntentParameterMetadataDefaultValueID + Ed4d2z + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Which path should be used? + INIntentParameterPromptDialogFormatStringID + uo96Pp + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${path}’. + INIntentParameterPromptDialogFormatStringID + Czb1hO + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + 9eAgmm + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${path}’? + INIntentParameterPromptDialogFormatStringID + wwyxxS + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + XVBTsw + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + fotVdI + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + zAG76o + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + iyefxU + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + dH8vD7 + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 6 + INIntentParameterType + Object + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Sort by + INIntentParameterDisplayNameID + IklOI3 + INIntentParameterDisplayPriority + 3 + INIntentParameterEnumType + SortingType + INIntentParameterEnumTypeNamespace + K5U8aR + INIntentParameterName + sortType + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${sortType}’. + INIntentParameterPromptDialogFormatStringID + a5zbSk + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + YkRxhp + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${sortType}’? + INIntentParameterPromptDialogFormatStringID + 2etfeF + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 8 + INIntentParameterType + Integer + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Sort Direction + INIntentParameterDisplayNameID + gFiypL + INIntentParameterDisplayPriority + 4 + INIntentParameterEnumType + SortingDirection + INIntentParameterEnumTypeNamespace + K5U8aR + INIntentParameterName + sortDirection + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${sortDirection}’. + INIntentParameterPromptDialogFormatStringID + ZXvi1J + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + d8ddSN + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${sortDirection}’? + INIntentParameterPromptDialogFormatStringID + 5tW7cP + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 10 + INIntentParameterType + Integer + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + xZDy1P + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + JPFL0e + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + N181Er + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + ZWoKzx + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + SP1wR2 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + QIKvUr + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 6JHJJP + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + pLbUOb + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + fjiVU2 + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + 0pF5fq + INIntentResponseCodeName + unlicensed + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + directoryListing + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Directory Listing + INIntentResponseParameterDisplayNameID + XEIycV + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + directoryListing + INIntentResponseParameterSupportsMultipleValues + + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + String + + + + INIntentTitle + Get Directory Listing + INIntentTitleID + PHyaws + INIntentType + Custom + INIntentVerb + Request + + + INIntentCategory + download + INIntentConfigurable + + INIntentDescription + Get file for the given path in account + INIntentDescriptionID + nvFl2W + INIntentIneligibleForSuggestions + + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 4 + INIntentManagedParameterCombinations + + path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get File ${path} from ${account} + INIntentParameterCombinationTitleID + kfmZUU + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetFile + INIntentParameters + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + eGjKAM + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + INIntentParameterMetadataDefaultValue + / + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Which File do you need? + INIntentParameterPromptDialogFormatStringID + 38884k + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + sxd9RH + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + VHlRdG + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + 9uzczk + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + 8xEyec + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + Ezlwwe + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 4 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + WDPKZp + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + NEiHz6 + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + sf57GO + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + lrF7G0 + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + 4eTrn2 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + 61YuKy + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + SW8AiH + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + cfhOuF + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + rzbAOP + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + 8a8rsQ + INIntentResponseCodeName + unlicensed + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + file + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + File + INIntentResponseParameterDisplayNameID + 08siGk + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + file + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + File + + + + INIntentTitle + Get File + INIntentTitleID + 8Ziepl + INIntentType + Custom + INIntentVerb + Get + + + INIntentCategory + set + INIntentConfigurable + + INIntentDescription + Save a file into the path in account + INIntentDescriptionID + dtUvRf + INIntentIneligibleForSuggestions + + INIntentInput + file + INIntentKeyParameter + file + INIntentLastParameterTag + 9 + INIntentManagedParameterCombinations + + path,file,account,filename,fileextension + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Save File ${file} to ${path} in ${account} + INIntentParameterCombinationTitleID + 6a3odp + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + SaveFile + INIntentParameters + + + INIntentParameterDisplayName + File + INIntentParameterDisplayNameID + YzkaES + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCustomUTIs + + + UTI + public.data + + + UTI + public.image + + + UTI + public.content + + + INIntentParameterMetadataType + Custom + + INIntentParameterName + file + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Which file to save? + INIntentParameterPromptDialogFormatStringID + ijKWiB + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 5 + INIntentParameterType + File + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + dlIKxE + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Where to save the file? + INIntentParameterPromptDialogFormatStringID + 5bvXKH + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + tiHX6L + INIntentParameterDisplayPriority + 3 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + e5LXg2 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + ogx9ZM + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + Q5StNV + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + o7aBS2 + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 7 + INIntentParameterType + Object + + + INIntentParameterDisplayName + File Name (optional) + INIntentParameterDisplayNameID + B1lmXQ + INIntentParameterDisplayPriority + 4 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + filename + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Optional file name for saving file + INIntentParameterPromptDialogFormatStringID + b4VIz0 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 8 + INIntentParameterType + String + + + INIntentParameterDisplayName + File Extension (optional) + INIntentParameterDisplayNameID + cDQ3EQ + INIntentParameterDisplayPriority + 5 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + fileextension + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Optional file extension for saving file + INIntentParameterPromptDialogFormatStringID + dRSMFQ + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 9 + INIntentParameterType + String + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + P6Xeu7 + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + 6JcpPy + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + xgwnia + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + leB73e + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + xIWWfW + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + AtUMKl + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + WfA15u + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + OmLLGX + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + 81IjYk + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + yTzyi1 + INIntentResponseCodeName + unlicensed + + + INIntentResponseLastParameterTag + 1 + INIntentResponseOutput + filePath + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + File Path + INIntentResponseParameterDisplayNameID + LJUrKB + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + filePath + INIntentResponseParameterTag + 1 + INIntentResponseParameterType + String + + + + INIntentTitle + Save File + INIntentTitleID + 1UkR2J + INIntentType + Custom + INIntentVerb + Set + + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Create a folder with the given name into the path. + INIntentDescriptionID + XmX9YS + INIntentIneligibleForSuggestions + + INIntentInput + name + INIntentKeyParameter + name + INIntentLastParameterTag + 4 + INIntentManagedParameterCombinations + + name,path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Create Folder ${name} in ${path} for ${account} + INIntentParameterCombinationTitleID + Rh6ICd + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + CreateFolder + INIntentParameters + + + INIntentParameterDisplayName + Name + INIntentParameterDisplayNameID + FZOxYk + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + name + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Name of the folder + INIntentParameterPromptDialogFormatStringID + W2jbjk + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + GOVcde + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Where to create the folder? + INIntentParameterPromptDialogFormatStringID + KHwW13 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 2 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + mRWXIl + INIntentParameterDisplayPriority + 3 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + 4BnO3u + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + Fe4YSc + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + LiNwxr + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + Y16qbv + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 4 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + ggYJ45 + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + OBFBYf + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + ctet5s + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + l1ScV6 + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + CtNzv7 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + dWm6eD + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + This folder already exists in the given path + INIntentResponseCodeConciseFormatStringID + 0n7uf3 + INIntentResponseCodeFormatString + This folder already exists in the given path + INIntentResponseCodeFormatStringID + Aviuqh + INIntentResponseCodeName + folderExistsFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + je9RXz + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + 4WGDPz + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + X0Zskf + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + bfU2dy + INIntentResponseCodeName + unlicensed + + + INIntentResponseLastParameterTag + 1 + INIntentResponseOutput + path + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Path + INIntentResponseParameterDisplayNameID + hL4u0G + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + path + INIntentResponseParameterTag + 1 + INIntentResponseParameterType + String + + + + INIntentTitle + Create Folder + INIntentTitleID + AQ4lsZ + INIntentType + Custom + INIntentVerb + Do + + + INIntentCategory + download + INIntentConfigurable + + INIntentDescription + Retrieve file infos for the given path + INIntentDescriptionID + LdHBeL + INIntentIneligibleForSuggestions + + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 3 + INIntentManagedParameterCombinations + + path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get File Info for ${path} in ${account} + INIntentParameterCombinationTitleID + 7Rco9W + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetFileInfo + INIntentParameters + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + unxhTD + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + For which path you want the file info? + INIntentParameterPromptDialogFormatStringID + 3uG0po + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + wPdYjH + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + xaKzfr + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + q5bi2K + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + upG52E + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + 7NRaX2 + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + 0xydIy + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + yeD8D7 + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + 3dRWka + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + sLw6L8 + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + lUzgvo + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + hLfV0h + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 4JjSYi + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + znA04X + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + idUnAy + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + lNNcvh + INIntentResponseCodeName + unlicensed + + + INIntentResponseLastParameterTag + 3 + INIntentResponseOutput + fileInfo + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + File Info + INIntentResponseParameterDisplayNameID + 4HFDB7 + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + fileInfo + INIntentResponseParameterObjectType + FileInfo + INIntentResponseParameterObjectTypeNamespace + K5U8aR + INIntentResponseParameterTag + 3 + INIntentResponseParameterType + Object + + + + INIntentTitle + Get File Info + INIntentTitleID + MGAYW2 + INIntentType + Custom + INIntentVerb + Get + + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Check if the given path exists + INIntentDescriptionID + PthGCb + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 3 + INIntentManagedParameterCombinations + + path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Check if path ${path} exists in ${account} + INIntentParameterCombinationTitleID + 79FdrK + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + PathExists + INIntentParameterCombinations + + path,account + + INIntentParameterCombinationIsLinked + + INIntentParameterCombinationIsPrimary + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Check if path ${path} exists in ${account} + INIntentParameterCombinationTitleID + 0jWfGg + + + INIntentParameters + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + 3allZk + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + For which path you want the file info? + INIntentParameterPromptDialogFormatStringID + vhZe7o + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + uP1q1e + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + odUmdN + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + eJ8Zop + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + c8L2xN + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + mJL3AY + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + eATsqP + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + tMYYpx + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + g1Zin1 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + C4qdxp + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + tiPumg + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + sdm6CF + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + 65dv1W + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + 9tIXKj + INIntentResponseCodeName + unlicensed + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + pathExists + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Path Exists + INIntentResponseParameterDisplayNameID + BOHYUX + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterMetadata + + INIntentResponseParameterMetadataFalseDisplayName + false + INIntentResponseParameterMetadataFalseDisplayNameID + bPAspK + INIntentResponseParameterMetadataTrueDisplayName + true + INIntentResponseParameterMetadataTrueDisplayNameID + 6xEiIO + + INIntentResponseParameterName + pathExists + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + Boolean + + + + INIntentTitle + Path Exists + INIntentTitleID + ds6RJ5 + INIntentType + Custom + INIntentVerb + Do + + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Delete the given path in account + INIntentDescriptionID + jkLupt + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 3 + INIntentManagedParameterCombinations + + path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Delete Item at path ${path} in ${account} + INIntentParameterCombinationTitleID + TjffcC + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + DeletePathItem + INIntentParameterCombinations + + path,account + + INIntentParameterCombinationIsLinked + + INIntentParameterCombinationIsPrimary + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Delete Item at path ${path} in ${account} + INIntentParameterCombinationTitleID + gYCA4r + + + INIntentParameters + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + ZhFjkx + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + For which path you want the file info? + INIntentParameterPromptDialogFormatStringID + qWXOGM + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + Iej0wL + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + YDTXfI + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + b4pgUO + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + HTGZys + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + mqKEhg + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + R54XCc + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + Iq98sA + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + Y87IjO + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + 1KMplM + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + 7leZJ9 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + ABZOUR + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 5bfpVi + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + 6oQ3E3 + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + sKUo1c + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + 1rpPPR + INIntentResponseCodeName + unlicensed + + + + INIntentTitle + Delete Path Item + INIntentTitleID + 30uTS2 + INIntentType + Custom + INIntentVerb + Do + + + INIntentCategory + download + INIntentConfigurable + + INIntentDescription + Get the account for the given account UUID + INIntentDescriptionID + U3l2bk + INIntentInput + accountUUID + INIntentKeyParameter + accountUUID + INIntentLastParameterTag + 1 + INIntentManagedParameterCombinations + + accountUUID + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get Account with ${accountUUID} + INIntentParameterCombinationTitleID + D3oUHR + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetAccount + INIntentParameterCombinations + + accountUUID + + INIntentParameterCombinationIsLinked + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get Account with ${accountUUID} + INIntentParameterCombinationTitleID + dCe7NY + + + INIntentParameters + + + INIntentParameterDisplayName + Account UUID + INIntentParameterDisplayNameID + Dt0MVQ + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + accountUUID + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Enter the UUID for the account you want + INIntentParameterPromptDialogFormatStringID + VYw9o4 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + AG0qbP + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + Vg8Ky0 + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + Kr0sps + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + UdyMQ5 + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 7bVLsg + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + 4U4XOu + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + ceG0fs + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + GJ25u2 + INIntentResponseCodeName + unlicensed + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + account + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Account + INIntentResponseParameterDisplayNameID + GAhtDQ + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + account + INIntentResponseParameterObjectType + Account + INIntentResponseParameterObjectTypeNamespace + K5U8aR + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + Object + + + + INIntentTitle + Get Account + INIntentTitleID + 8ZZD23 + INIntentType + Custom + INIntentVerb + Get + + + INTypes + + + INTypeDisplayName + Account + INTypeDisplayNameID + Cfk0HW + INTypeLastPropertyTag + 103 + INTypeName + Account + INTypeProperties + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 1 + INTypePropertyName + identifier + INTypePropertyTag + 1 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 2 + INTypePropertyName + displayString + INTypePropertyTag + 2 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 3 + INTypePropertyName + pronunciationHint + INTypePropertyTag + 3 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 4 + INTypePropertyName + alternativeSpeakableMatches + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 4 + INTypePropertyType + SpeakableString + + + INTypePropertyDisplayName + Server URL + INTypePropertyDisplayNameID + 3BsNkC + INTypePropertyDisplayPriority + 5 + INTypePropertyName + serverURL + INTypePropertyTag + 101 + INTypePropertyType + URL + + + INTypePropertyDisplayName + Name + INTypePropertyDisplayNameID + ZRW2EV + INTypePropertyDisplayPriority + 6 + INTypePropertyName + name + INTypePropertyTag + 102 + INTypePropertyType + String + + + INTypePropertyDisplayName + Uuid + INTypePropertyDisplayNameID + gDQ2EW + INTypePropertyDisplayPriority + 7 + INTypePropertyName + uuid + INTypePropertyTag + 103 + INTypePropertyType + String + + + + + INTypeDisplayName + File Info + INTypeDisplayNameID + A3Pv0a + INTypeLastPropertyTag + 114 + INTypeName + FileInfo + INTypeProperties + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 1 + INTypePropertyName + identifier + INTypePropertyTag + 1 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 2 + INTypePropertyName + displayString + INTypePropertyTag + 2 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 3 + INTypePropertyName + pronunciationHint + INTypePropertyTag + 3 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 4 + INTypePropertyName + alternativeSpeakableMatches + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 4 + INTypePropertyType + SpeakableString + + + INTypePropertyDisplayName + Last Modified + INTypePropertyDisplayNameID + qD6IIc + INTypePropertyDisplayPriority + 5 + INTypePropertyMetadata + + INTypePropertyMetadataDateStyle + Full + INTypePropertyMetadataFormat + Relative + INTypePropertyMetadataTimeStyle + Full + INTypePropertyMetadataType + DateTime + + INTypePropertyName + lastModified + INTypePropertyTag + 110 + INTypePropertyType + DateComponents + + + INTypePropertyDisplayName + Is Favorite + INTypePropertyDisplayNameID + lIGrzU + INTypePropertyDisplayPriority + 6 + INTypePropertyMetadata + + INTypePropertyMetadataFalseDisplayName + false + INTypePropertyMetadataFalseDisplayNameID + eg3e3D + INTypePropertyMetadataTrueDisplayName + true + INTypePropertyMetadataTrueDisplayNameID + KpcaUP + + INTypePropertyName + isFavorite + INTypePropertyTag + 109 + INTypePropertyType + Boolean + + + INTypePropertyDisplayName + Size + INTypePropertyDisplayNameID + wbCXnN + INTypePropertyDisplayPriority + 7 + INTypePropertyName + size + INTypePropertyTag + 107 + INTypePropertyType + Integer + + + INTypePropertyDisplayName + Creation Date + INTypePropertyDisplayNameID + 38uTf2 + INTypePropertyDisplayPriority + 8 + INTypePropertyMetadata + + INTypePropertyMetadataDateStyle + Full + INTypePropertyMetadataFormat + Relative + INTypePropertyMetadataTimeStyle + Full + INTypePropertyMetadataType + DateTime + + INTypePropertyName + creationDate + INTypePropertyTag + 106 + INTypePropertyType + DateComponents + + + INTypePropertyDisplayName + Mime Type + INTypePropertyDisplayNameID + zKRAWH + INTypePropertyDisplayPriority + 9 + INTypePropertyName + mimeType + INTypePropertyTag + 105 + INTypePropertyType + String + + + INTypePropertyDisplayName + Last Modified Timestamp + INTypePropertyDisplayNameID + lyQhRe + INTypePropertyDisplayPriority + 10 + INTypePropertyName + lastModifiedTimestamp + INTypePropertyTag + 112 + INTypePropertyType + Integer + + + INTypePropertyDisplayName + Creation Date Timestamp + INTypePropertyDisplayNameID + MRTQ9c + INTypePropertyDisplayPriority + 11 + INTypePropertyName + creationDateTimestamp + INTypePropertyTag + 114 + INTypePropertyType + Integer + + + + + + diff --git a/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift b/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift new file mode 100644 index 000000000..c0c952bfe --- /dev/null +++ b/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift @@ -0,0 +1,119 @@ +// +// CreateFolderIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 31.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { + + public func handle(intent: CreateFolderIntent, completion: @escaping (CreateFolderIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(CreateFolderIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path?.pathRepresentation, let uuid = intent.account?.uuid, let name = intent.name else { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(CreateFolderIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let targetItem = item { + let folderPath = String(format: "%@%@", path, name) + // Check, if the folder already exists in the given path + OCItemTracker().item(for: bookmark, at: folderPath) { (error, core, folderPathItem) in + if error == nil, folderPathItem == nil, let core = core { + + let progress = core.createFolder(name, inside: targetItem, options: nil, resultHandler: { (error, _, item, _) in + if error != nil { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + } else { + completion(CreateFolderIntentResponse.success(path: item?.path ?? "")) + } + }) + + if progress == nil { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + } + } else if core != nil { + completion(CreateFolderIntentResponse(code: .folderExistsFailure, userActivity: nil)) + } else { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + } + } + } else if core != nil { + completion(CreateFolderIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolveName(for intent: CreateFolderIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let name = intent.name { + completion(INStringResolutionResult.success(with: name)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + public func resolveAccount(for intent: CreateFolderIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func provideAccountOptions(for intent: CreateFolderIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolvePath(for intent: CreateFolderIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } +} + +@available(iOS 13.0, *) +extension CreateFolderIntentResponse { + + public static func success(path: String) -> CreateFolderIntentResponse { + let intentResponse = CreateFolderIntentResponse(code: .success, userActivity: nil) + intentResponse.path = path + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift b/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift new file mode 100644 index 000000000..a40c8c9db --- /dev/null +++ b/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift @@ -0,0 +1,90 @@ +// +// DeletePathItemIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class DeletePathItemIntentHandler: NSObject, DeletePathItemIntentHandling { + + public func handle(intent: DeletePathItemIntent, completion: @escaping (DeletePathItemIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(DeletePathItemIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path, let uuid = intent.account?.uuid else { + completion(DeletePathItemIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(DeletePathItemIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let targetItem = item, let core = core { + let progress = core.delete(targetItem, requireMatch: true, resultHandler: { (error, _, _, _) in + if error != nil { + completion(DeletePathItemIntentResponse(code: .failure, userActivity: nil)) + } else { + completion(DeletePathItemIntentResponse(code: .success, userActivity: nil)) + } + }) + + if progress == nil { + completion(DeletePathItemIntentResponse(code: .failure, userActivity: nil)) + } + } else if core != nil { + completion(DeletePathItemIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(DeletePathItemIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolveAccount(for intent: DeletePathItemIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func provideAccountOptions(for intent: DeletePathItemIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolvePath(for intent: DeletePathItemIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } +} diff --git a/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift b/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift new file mode 100644 index 000000000..067e2434e --- /dev/null +++ b/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift @@ -0,0 +1,56 @@ +// +// Account+Extension.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 29.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import UIKit +import ownCloudSDK + +@available(iOS 13.0, *) +extension OCBookmarkManager { + + public var accountList : [Account] { + var accountList : [Account] = [] + accountList = OCBookmarkManager.shared.bookmarks.map { (bookmark) -> Account in + let account = Account(identifier: bookmark.uuid.uuidString, display: bookmark.shortName) + account.name = bookmark.shortName + account.serverURL = bookmark.url + account.uuid = bookmark.uuid.uuidString + + return account + } + + return accountList + } + + public func bookmark(for uuidString: String) -> OCBookmark? { + return OCBookmarkManager.shared.bookmarks.filter({ $0.uuid.uuidString == uuidString}).first + } + + public func accountBookmark(for uuidString: String) -> Account? { + if let bookmark = bookmark(for: uuidString) { + let account = Account(identifier: bookmark.uuid.uuidString, display: bookmark.shortName) + account.name = bookmark.shortName + account.serverURL = bookmark.url + account.uuid = bookmark.uuid.uuidString + + return account + } + + return nil + } + +} diff --git a/ownCloudAppShared/Intent/GetAccountIntentHandler.swift b/ownCloudAppShared/Intent/GetAccountIntentHandler.swift new file mode 100644 index 000000000..fb39d5518 --- /dev/null +++ b/ownCloudAppShared/Intent/GetAccountIntentHandler.swift @@ -0,0 +1,75 @@ +// +// GetAccountsIntentHandler.swift +// ownCloud +// +// Created by Matthias Hühne on 29.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class GetAccountIntentHandler: NSObject, GetAccountIntentHandling { + + public func handle(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetAccountIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let uuid = intent.accountUUID else { + completion(GetAccountIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let accountBookmark = OCBookmarkManager.shared.accountBookmark(for: uuid) else { + completion(GetAccountIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + completion(GetAccountIntentResponse.success(account: accountBookmark)) + } + + public func resolveAccountUUID(for intent: GetAccountIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let account = intent.accountUUID { + completion(INStringResolutionResult.success(with: account)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + public func confirm(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { + completion(GetAccountIntentResponse(code: .ready, userActivity: nil)) + } +} + +@available(iOS 13.0, *) +extension GetAccountIntentResponse { + + public static func success(account: Account) -> GetAccountIntentResponse { + let intentResponse = GetAccountIntentResponse(code: .success, userActivity: nil) + intentResponse.account = account + return intentResponse + } + +} diff --git a/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift b/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift new file mode 100644 index 000000000..b0ecdde50 --- /dev/null +++ b/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift @@ -0,0 +1,57 @@ +// +// GetAccountsIntentHandler.swift +// ownCloud +// +// Created by Matthias Hühne on 24.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class GetAccountsIntentHandler: NSObject, GetAccountsIntentHandling { + + public func handle(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetAccountsIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + completion(GetAccountsIntentResponse.success(accountList: OCBookmarkManager.shared.accountList)) + } + + public func confirm(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { + completion(GetAccountsIntentResponse(code: .ready, userActivity: nil)) + } +} + +@available(iOS 13.0, *) +extension GetAccountsIntentResponse { + + public static func success(accountList: [Account]) -> GetAccountsIntentResponse { + let intentResponse = GetAccountsIntentResponse(code: .success, userActivity: nil) + intentResponse.accountList = accountList + return intentResponse + } + +} diff --git a/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift b/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift new file mode 100644 index 000000000..dee1e2731 --- /dev/null +++ b/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift @@ -0,0 +1,135 @@ +// +// GetDirectoryIntentHandler.swift +// ownCloud +// +// Created by Matthias Hühne on 24.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +typealias GetDirectoryListingCompletionHandler = (GetDirectoryListingIntentResponse) -> Void + +@available(iOS 13.0, *) +public class GetDirectoryListingIntentHandler: NSObject, GetDirectoryListingIntentHandling, OCQueryDelegate { + + var completion : GetDirectoryListingCompletionHandler? + + public func resolvePath(for intent: GetDirectoryListingIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + + } + + public func provideAccountOptions(for intent: GetDirectoryListingIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolveAccount(for intent: GetDirectoryListingIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func resolveSortType(for intent: GetDirectoryListingIntent, with completion: @escaping (SortingTypeResolutionResult) -> Void) { + completion(SortingTypeResolutionResult.success(with: intent.sortType)) + } + + public func resolveSortDirection(for intent: GetDirectoryListingIntent, with completion: @escaping (SortingDirectionResolutionResult) -> Void) { + completion(SortingDirectionResolutionResult.success(with: intent.sortDirection)) + } + + public func handle(intent: GetDirectoryListingIntent, completion: @escaping (GetDirectoryListingIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetDirectoryListingIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path?.pathRepresentation, let uuid = intent.account?.uuid else { + completion(GetDirectoryListingIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(GetDirectoryListingIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + self.completion = completion + + OCCoreManager.shared.requestCore(for: bookmark, setup: nil, completionHandler: { (core, error) in + if error == nil { + let targetDirectoryQuery = OCQuery(forPath: path) + targetDirectoryQuery.delegate = self + + if targetDirectoryQuery.sortComparator == nil { + let sort = SortMethod(rawValue: (intent.sortType.rawValue - 1)) ?? SortMethod.alphabetically + + targetDirectoryQuery.sortComparator = sort.comparator(direction: SortDirection(rawValue: (intent.sortDirection.rawValue - 1)) ?? SortDirection.ascendant) + } + core?.start(targetDirectoryQuery) + } else { + self.completion?(GetDirectoryListingIntentResponse(code: .failure, userActivity: nil)) + } + }) + } + + public func queryHasChangesAvailable(_ query: OCQuery) { + if query.state == .targetRemoved { + self.completion?(GetDirectoryListingIntentResponse(code: .pathFailure, userActivity: nil)) + self.completion = nil + } else if query.state == .idle { + var directoryListing : [String] = [] + if let results = query.queryResults { + directoryListing = results.compactMap { return $0.path } + } + + self.completion?(GetDirectoryListingIntentResponse.success(directoryListing: directoryListing)) + self.completion = nil + } + } + + public func query(_ query: OCQuery, failedWithError error: Error) { + self.completion?(GetDirectoryListingIntentResponse(code: .failure, userActivity: nil)) + } + + public func confirm(intent: GetDirectoryListingIntent, completion: @escaping (GetDirectoryListingIntentResponse) -> Void) { + completion(GetDirectoryListingIntentResponse(code: .ready, userActivity: nil)) + } +} + +@available(iOS 13.0, *) +extension GetDirectoryListingIntentResponse { + + public static func success(directoryListing: [String]) -> GetDirectoryListingIntentResponse { + let intentResponse = GetDirectoryListingIntentResponse(code: .success, userActivity: nil) + intentResponse.directoryListing = directoryListing + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift b/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift new file mode 100644 index 000000000..8137808af --- /dev/null +++ b/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift @@ -0,0 +1,108 @@ +// +// GetFileInfoIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 27.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class GetFileInfoIntentHandler: NSObject, GetFileInfoIntentHandling { + + public func handle(intent: GetFileInfoIntent, completion: @escaping (GetFileInfoIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetFileInfoIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path, let uuid = intent.account?.uuid else { + completion(GetFileInfoIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(GetFileInfoIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let targetItem = item { + let fileInfo = FileInfo(identifier: targetItem.localID, display: targetItem.name ?? "") + + let calendar = Calendar.current + if let creationDate = targetItem.creationDate { + let components = calendar.dateTimeComponents(from: creationDate) + fileInfo.creationDate = components + fileInfo.creationDateTimestamp = NSNumber(value: creationDate.timeIntervalSince1970) + } + if let lastModified = targetItem.lastModified { + let components = calendar.dateTimeComponents(from: lastModified) + fileInfo.lastModified = components + fileInfo.lastModifiedTimestamp = NSNumber(value: lastModified.timeIntervalSince1970) + } + fileInfo.isFavorite = targetItem.isFavorite + fileInfo.mimeType = targetItem.mimeType + fileInfo.size = NSNumber(value: targetItem.size) + + completion(GetFileInfoIntentResponse.success(fileInfo: fileInfo)) + } else if core != nil { + completion(GetFileInfoIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(GetFileInfoIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolveAccount(for intent: GetFileInfoIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func provideAccountOptions(for intent: GetFileInfoIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolvePath(for intent: GetFileInfoIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + +} + +@available(iOS 13.0, *) +extension GetFileInfoIntentResponse { + + public static func success(fileInfo: FileInfo) -> GetFileInfoIntentResponse { + let intentResponse = GetFileInfoIntentResponse(code: .success, userActivity: nil) + intentResponse.fileInfo = fileInfo + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/GetFileIntentHandler.swift b/ownCloudAppShared/Intent/GetFileIntentHandler.swift new file mode 100644 index 000000000..cc9bd814a --- /dev/null +++ b/ownCloudAppShared/Intent/GetFileIntentHandler.swift @@ -0,0 +1,106 @@ +// +// GetFileIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +typealias GetFileCompletionHandler = (GetFileIntentResponse) -> Void + +@available(iOS 13.0, *) +public class GetFileIntentHandler: NSObject, GetFileIntentHandling { + + public func handle(intent: GetFileIntent, completion: @escaping (GetFileIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetFileIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path, let uuid = intent.account?.uuid else { + completion(GetFileIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(GetFileIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let item = item { + if core?.localCopy(of: item) == nil { + core?.downloadItem(item, options: [ .returnImmediatelyIfOfflineOrUnavailable : true ], resultHandler: { (error, core, item, file) in + if error == nil, let item = item, let file = item.file(with: core), let url = file.url { + let file = INFile(fileURL: url, filename: item.name, typeIdentifier: nil) + completion(GetFileIntentResponse.success(file: file)) + } else { + completion(GetFileIntentResponse(code: .failure, userActivity: nil)) + } + }) + } else if let core = core, let file = item.file(with: core), let url = file.url { + let file = INFile(fileURL: url, filename: item.name, typeIdentifier: nil) + completion(GetFileIntentResponse.success(file: file)) + } + } else if core != nil { + completion(GetFileIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(GetFileIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolvePath(for intent: GetFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + public func provideAccountOptions(for intent: GetFileIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolveAccount(for intent: GetFileIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } +} + +@available(iOS 13.0, *) +extension GetFileIntentResponse { + + public static func success(file: INFile) -> GetFileIntentResponse { + let intentResponse = GetFileIntentResponse(code: .success, userActivity: nil) + intentResponse.file = file + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/PathExistsIntentHandler.swift b/ownCloudAppShared/Intent/PathExistsIntentHandler.swift new file mode 100644 index 000000000..1742d22ef --- /dev/null +++ b/ownCloudAppShared/Intent/PathExistsIntentHandler.swift @@ -0,0 +1,90 @@ +// +// PathExistsIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class PathExistsIntentHandler: NSObject, PathExistsIntentHandling { + + public func handle(intent: PathExistsIntent, completion: @escaping (PathExistsIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(PathExistsIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path, let uuid = intent.account?.uuid else { + completion(PathExistsIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(PathExistsIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, item != nil { + completion(PathExistsIntentResponse.success(pathExists: true)) + } else if core != nil { + completion(PathExistsIntentResponse.success(pathExists: false)) + } else { + completion(PathExistsIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolveAccount(for intent: PathExistsIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func provideAccountOptions(for intent: PathExistsIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolvePath(for intent: PathExistsIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } +} + +@available(iOS 13.0, *) +extension PathExistsIntentResponse { + + public static func success(pathExists: Bool) -> PathExistsIntentResponse { + let intentResponse = PathExistsIntentResponse(code: .success, userActivity: nil) + intentResponse.pathExists = NSNumber(value: pathExists) + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/SaveFileIntentHandler.swift b/ownCloudAppShared/Intent/SaveFileIntentHandler.swift new file mode 100644 index 000000000..ddc6da7af --- /dev/null +++ b/ownCloudAppShared/Intent/SaveFileIntentHandler.swift @@ -0,0 +1,141 @@ +// +// SaveFileIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import UIKit +import Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { + + public func handle(intent: SaveFileIntent, completion: @escaping (SaveFileIntentResponse) -> Void) { + + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + + guard !AppLockHelper().isPassCodeEnabled else { + completion(SaveFileIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path?.pathRepresentation, let uuid = intent.account?.uuid, let file = intent.file, let fileURL = file.fileURL else { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(SaveFileIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + var newFilename = file.filename + if let filename = intent.filename as NSString?, filename.length > 0, let defaultFilename = file.filename as NSString? { + var pathExtention = defaultFilename.pathExtension + if let fileExtension = intent.fileextension, fileExtension.count > 0 { + pathExtention = fileExtension + } + if let changedFilename = filename.appendingPathExtension(pathExtention) { + newFilename = changedFilename + } + } else if let fileExtension = intent.fileextension, fileExtension.count > 0, let defaultFilename = file.filename as NSString? { + let filename = defaultFilename.deletingPathExtension as NSString + if let changedFilename = filename.appendingPathExtension(fileExtension) { + newFilename = changedFilename + } + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let targetItem = item { + if core?.importFileNamed(newFilename, + at: targetItem, + from: fileURL, + isSecurityScoped: true, + options: [OCCoreOption.importByCopying : true], + placeholderCompletionHandler: { (error, item) in + if error != nil { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + } + }, + resultHandler: { (error, _ core, _ item, _) in + if error != nil { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + } else { + completion(SaveFileIntentResponse.success(filePath: item?.path ?? "")) + } + } + ) == nil { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + } + } else if core != nil { + completion(SaveFileIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func provideAccountOptions(for intent: SaveFileIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolveAccount(for intent: SaveFileIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func resolvePath(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + public func resolveFile(for intent: SaveFileIntent, with completion: @escaping (INFileResolutionResult) -> Void) { + if let file = intent.file { + completion(INFileResolutionResult.success(with: file)) + } else { + completion(INFileResolutionResult.needsValue()) + } + } + + public func resolveFilename(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + completion(INStringResolutionResult.success(with: intent.filename ?? "")) + } + + public func resolveFileextension(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + completion(INStringResolutionResult.success(with: intent.fileextension ?? "")) + } +} + +@available(iOS 13.0, *) +extension SaveFileIntentResponse { + + public static func success(filePath: String) -> SaveFileIntentResponse { + let intentResponse = SaveFileIntentResponse(code: .success, userActivity: nil) + intentResponse.filePath = filePath + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/en.lproj/Intents.strings b/ownCloudAppShared/Intent/en.lproj/Intents.strings new file mode 100644 index 000000000..00af48a78 --- /dev/null +++ b/ownCloudAppShared/Intent/en.lproj/Intents.strings @@ -0,0 +1,92 @@ +"0jWfGg" = "Check if path ${path} exists in ${account}"; + +"1KMplM" = "The given path does not exists"; + +"1UkR2J" = "Save File"; + +"2IChwb" = "Get a directory listing for the given account and path"; + +"30uTS2" = "Delete Path Item"; + +"61YuKy" = "The account with the given UUID does not exists"; + +"6JcpPy" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"8ZZD23" = "Get Account"; + +"8Ziepl" = "Get File"; + +"ABZOUR" = "The account with the given UUID does not exists"; + +"AQ4lsZ" = "Create Folder"; + +"AtUMKl" = "The account with the given UUID does not exists"; + +"C4qdxp" = "The account with the given UUID does not exists"; + +"Iq98sA" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"JPFL0e" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"LdHBeL" = "Retrieve file infos for the given path"; + +"MGAYW2" = "Get File Info"; + +"NEiHz6" = "The given path does not exists"; + +"OBFBYf" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"PHyaws" = "Get Directory Listing"; + +"PthGCb" = "Check if the given path exists"; + +"QIKvUr" = "The account with the given UUID does not exists"; + +"U3l2bk" = "Get the account for the given account UUID"; + +"UdyMQ5" = "The account with the given UUID does not exists"; + +"Vg8Ky0" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"XKCjDe" = "Get Accounts"; + +"XmX9YS" = "Create a folder with the given name into the path."; + +"ZWoKzx" = "The given path does not exists"; + +"dCe7NY" = "Get Account with ${accountUUID}"; + +"dWm6eD" = "The account with the given UUID does not exists"; + +"ds6RJ5" = "Path Exists"; + +"dtUvRf" = "Save a file into the path in account"; + +"gYCA4r" = "Delete Item at path ${path} in ${account}"; + +"gxBJkl" = "Here is your account list"; + +"hLfV0h" = "The account with the given UUID does not exists"; + +"iDEwxe" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"jkLupt" = "Delete the given path in account"; + +"l1ScV6" = "The given path does not exists"; + +"leB73e" = "The given path does not exists"; + +"lrF7G0" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"nvFl2W" = "Get file for the given path in account"; + +"sLw6L8" = "The given path does not exists"; + +"tMYYpx" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"wK4bqS" = "Get a list of all configured accounts"; + +"yeD8D7" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"yrLv2M" = "Here is your account list"; + diff --git a/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift b/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift new file mode 100644 index 000000000..a1a5eb124 --- /dev/null +++ b/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift @@ -0,0 +1,339 @@ +// +// OCItem+Extension.swift +// ownCloud +// +// Created by Felix Schwarz on 13.04.18. +// Copyright © 2018 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2018, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK + +extension OCItem { + static private let iconNamesByMIMEType : [String:String] = { + var mimeTypeToIconMap : [String:String] = [ + // List taken from https://github.com/owncloud/core/blob/master/core/js/mimetypelist.js + "application/coreldraw": "image", + "application/epub+zip": "text", + "application/font-sfnt": "image", + "application/font-woff": "image", + "application/illustrator": "image", + "application/javascript": "text/code", + "application/json": "text/code", + "application/msaccess": "file", + "application/msexcel": "x-office/spreadsheet", + "application/msonenote": "x-office/document", + "application/mspowerpoint": "x-office/presentation", + "application/msword": "x-office/document", + "application/octet-stream": "file", + "application/postscript": "image", + "application/rss+xml": "application/xml", + "application/vnd.android.package-archive": "package/x-generic", + "application/vnd.lotus-wordpro": "x-office/document", + "application/vnd.ms-excel": "x-office/spreadsheet", + "application/vnd.ms-excel.addin.macroEnabled.12": "x-office/spreadsheet", + "application/vnd.ms-excel.sheet.binary.macroEnabled.12": "x-office/spreadsheet", + "application/vnd.ms-excel.sheet.macroEnabled.12": "x-office/spreadsheet", + "application/vnd.ms-excel.template.macroEnabled.12": "x-office/spreadsheet", + "application/vnd.ms-fontobject": "image", + "application/vnd.ms-powerpoint": "x-office/presentation", + "application/vnd.ms-powerpoint.addin.macroEnabled.12": "x-office/presentation", + "application/vnd.ms-powerpoint.presentation.macroEnabled.12": "x-office/presentation", + "application/vnd.ms-powerpoint.slideshow.macroEnabled.12": "x-office/presentation", + "application/vnd.ms-powerpoint.template.macroEnabled.12": "x-office/presentation", + "application/vnd.ms-word.document.macroEnabled.12": "x-office/document", + "application/vnd.ms-word.template.macroEnabled.12": "x-office/document", + "application/vnd.oasis.opendocument.presentation": "x-office/presentation", + "application/vnd.oasis.opendocument.presentation-template": "x-office/presentation", + "application/vnd.oasis.opendocument.spreadsheet": "x-office/spreadsheet", + "application/vnd.oasis.opendocument.spreadsheet-template": "x-office/spreadsheet", + "application/vnd.oasis.opendocument.text": "x-office/document", + "application/vnd.oasis.opendocument.text-master": "x-office/document", + "application/vnd.oasis.opendocument.text-template": "x-office/document", + "application/vnd.oasis.opendocument.text-web": "x-office/document", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": "x-office/presentation", + "application/vnd.openxmlformats-officedocument.presentationml.slideshow": "x-office/presentation", + "application/vnd.openxmlformats-officedocument.presentationml.template": "x-office/presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "x-office/spreadsheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template": "x-office/spreadsheet", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "x-office/document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.template": "x-office/document", + "application/vnd.visio": "x-office/document", + "application/vnd.wordperfect": "x-office/document", + "application/x-7z-compressed": "package/x-generic", + "application/x-bzip2": "package/x-generic", + "application/x-cbr": "text", + "application/x-compressed": "package/x-generic", + "application/x-dcraw": "image", + "application/x-deb": "package/x-generic", + "application/x-font": "image", + "application/x-gimp": "image", + "application/x-gzip": "package/x-generic", + "application/x-perl": "text/code", + "application/x-photoshop": "image", + "application/x-php": "text/code", + "application/x-rar-compressed": "package/x-generic", + "application/x-tar": "package/x-generic", + "application/x-tex": "text", + "application/xml": "text/html", + "application/yaml": "text/code", + "application/zip": "package/x-generic", + "database": "file", + "httpd/unix-directory": "dir", + "message/rfc822": "text", + "text/css": "text/code", + "text/csv": "x-office/spreadsheet", + "text/html": "text/code", + "text/x-c": "text/code", + "text/x-c++src": "text/code", + "text/x-h": "text/code", + "text/x-java-source": "text/code", + "text/x-python": "text/code", + "text/x-shellscript": "text/code", + "web": "text/code" + ] + + mimeTypeToIconMap.keys.forEach { + let mimeTypeKey = $0 + var mimeType : String? = mimeTypeToIconMap[mimeTypeKey] + var referenceMIMEType : String? = mimeType + + while referenceMIMEType != nil { + referenceMIMEType = mimeTypeToIconMap[referenceMIMEType!] + + if let validMIMEType = referenceMIMEType { + mimeType = validMIMEType + } + } + + mimeTypeToIconMap[mimeTypeKey] = mimeType?.replacingOccurrences(of: "/", with: "-") + } + + return mimeTypeToIconMap + }() + + static private let validIconNames : [String] = [ + // List taken from https://github.com/owncloud/core/blob/master/core/js/mimetypelist.js + "application", + "application-pdf", + "audio", + "file", + "folder", + "folder-create", + "folder-drag-accept", + "folder-external", + "folder-public", + "folder-shared", + "folder-starred", + "image", + "package-x-generic", + "text", + "text-calendar", + "text-code", + "text-vcard", + "video", + "x-office-document", + "x-office-presentation", + "x-office-spreadsheet", + "icon-search" + ] + + static func iconName(for MIMEType: String?) -> String? { + var iconName : String? + + if let mimeType = MIMEType { + iconName = self.iconNamesByMIMEType[mimeType] + + if iconName != nil { + if !(self.validIconNames.contains(iconName!)) { + iconName = nil + } + } + + if iconName == nil { + let flatMIMEType = mimeType.replacingOccurrences(of: "/", with: "-") + + if self.validIconNames.contains(flatMIMEType) { + iconName = flatMIMEType + } else { + if let mimeCategory = mimeType.components(separatedBy: "/").first { + if mimeCategory != "application" { + if self.validIconNames.contains(mimeCategory) { + iconName = mimeCategory + } + } + } + } + } + } + + return iconName + } + + var iconName : String? { + var iconName = OCItem.iconName(for: self.mimeType) + + if iconName == nil { + if self.type == .collection { + iconName = "folder" + } else { + iconName = "file" + } + } + + return iconName + } + + var fileExtension : String? { + return (self.name as NSString?)?.pathExtension + } + + var baseName : String? { + return (self.name as NSString?)?.deletingPathExtension + } + + var sizeLocalized: String { + return OCItem.byteCounterFormatter.string(fromByteCount: Int64(self.size)) + } + + var lastModifiedLocalized: String { + guard let lastModified = self.lastModified else { return "" } + + return OCItem.dateFormatter.string(from: lastModified) + } + + static private let byteCounterFormatter: ByteCountFormatter = { + let byteCounterFormatter = ByteCountFormatter() + byteCounterFormatter.allowsNonnumericFormatting = false + return byteCounterFormatter + }() + + static private let dateFormatter: DateFormatter = { + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .medium + dateFormatter.locale = Locale.current + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + }() + + var sharedByPublicLink : Bool { + if self.shareTypesMask.contains(.link) { + return true + } + return false + } + + var isShared : Bool { + if self.shareTypesMask.isEmpty { + return false + } + return true + } + + var sharedByUserOrGroup : Bool { + if self.shareTypesMask.contains(.userShare) || self.shareTypesMask.contains(.groupShare) || self.shareTypesMask.contains(.remote) { + return true + } + return false + } + + func shareRootItem(from core: OCCore) -> OCItem? { + var shareRootItem : OCItem? + + if self.isSharedWithUser { + var parentItem : OCItem? = self + + shareRootItem = self + + repeat { + parentItem = parentItem?.parentItem(from: core) + + if parentItem != nil, parentItem?.isSharedWithUser == true { + shareRootItem = parentItem + } + } while ((parentItem != nil) && (parentItem?.isSharedWithUser == true)) + } + + return shareRootItem + } + + func isShareRootItem(from core: OCCore) -> Bool { + if let shareRootItem = shareRootItem(from: core) { + return shareRootItem.localID == localID + } + + return false + } + + func parentItem(from core: OCCore, completionHandler: ((_ error: Error?, _ parentItem: OCItem?) -> Void)? = nil) -> OCItem? { + var parentItem : OCItem? + + if let parentItemLocalID = self.parentLocalID { + var waitGroup : DispatchGroup? + + if completionHandler == nil { + waitGroup = DispatchGroup() + waitGroup?.enter() + } + + core.retrieveItemFromDatabase(forLocalID: parentItemLocalID) { (error, _, item) in + if parentItem == nil, let parentPath = self.path?.parentPath { + parentItem = try? core.cachedItem(atPath: parentPath) + } + + if completionHandler == nil { + parentItem = item + waitGroup?.leave() + } else { + completionHandler?(error, item) + } + } + + waitGroup?.wait() + } + + return parentItem + } + + func displaysDifferent(than item: OCItem?, in core: OCCore? = nil) -> Bool { + guard let item = item else { + return true + } + + return ( + // Different item + (item.localID != localID) || + + // File contents (and therefore likely metadata) differs + (item.itemVersionIdentifier != itemVersionIdentifier) || + + // File name differs + (item.name != name) || + + // Upload/Download status differs + (item.syncActivity != syncActivity) || + + // Cloud status differs + (item.cloudStatus != cloudStatus) || + + // Available offline status differs + (item.downloadTriggerIdentifier != downloadTriggerIdentifier) || + (core?.availableOfflinePolicyCoverage(of: item) != core?.availableOfflinePolicyCoverage(of: self)) || + + // Sharing attributes differ + (item.shareTypesMask != shareTypesMask) || + (item.permissions != permissions) // these contain sharing info, too + ) + } +} diff --git a/ownCloudAppShared/SDK Extensions/OCItemTracker.swift b/ownCloudAppShared/SDK Extensions/OCItemTracker.swift new file mode 100644 index 000000000..e2bb796af --- /dev/null +++ b/ownCloudAppShared/SDK Extensions/OCItemTracker.swift @@ -0,0 +1,41 @@ +// +// OCItemTracker.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 26.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK + +class OCItemTracker: NSObject { + + var itemTracking : OCCoreItemTracking? + + public func item(for bookmark: OCBookmark, at path: String, completionHandler: @escaping (_ error: Error?, _ core: OCCore?, _ item: OCItem?) -> Void) { + + OCCoreManager.shared.requestCore(for: bookmark, setup: nil, completionHandler: { (core, error) in + if error == nil, let core = core { + self.itemTracking = core.trackItem(atPath: path, trackingHandler: { (error, item, isInitial) in + if isInitial { + self.itemTracking = nil + } + completionHandler(error, core, item) + }) + } else { + completionHandler(error, nil, nil) + } + }) + } +} diff --git a/ownCloudAppShared/Tools/AppLockHelper.swift b/ownCloudAppShared/Tools/AppLockHelper.swift new file mode 100644 index 000000000..b1ecd0c07 --- /dev/null +++ b/ownCloudAppShared/Tools/AppLockHelper.swift @@ -0,0 +1,35 @@ +// +// AppLockHelper.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 29.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK + +class AppLockHelper: NSObject { + + var isPassCodeEnabled : Bool { + get { + let defaults = OCAppIdentity.shared.userDefaults + if let applockEnabled = defaults?.bool(forKey: "applock-lock-enabled") { + return applockEnabled + } + + return false + } + } + +} diff --git a/ownCloudAppShared/UIKit Extension/Calendar+Extension.swift b/ownCloudAppShared/UIKit Extension/Calendar+Extension.swift new file mode 100644 index 000000000..b96ac89eb --- /dev/null +++ b/ownCloudAppShared/UIKit Extension/Calendar+Extension.swift @@ -0,0 +1,26 @@ +// +// Calendar+Extension.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 27.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit + +extension Calendar { + func dateTimeComponents(from date: Date) -> DateComponents { + let components : Set = [.day, .month, .year, .hour, .minute, .second] + return self.dateComponents(components, from: date) + } +} diff --git a/ownCloud/UIKit Extensions/String+Extension.swift b/ownCloudAppShared/UIKit Extension/String+Extension.swift similarity index 81% rename from ownCloud/UIKit Extensions/String+Extension.swift rename to ownCloudAppShared/UIKit Extension/String+Extension.swift index 1492056b6..9060f0acc 100644 --- a/ownCloud/UIKit Extensions/String+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/String+Extension.swift @@ -21,7 +21,7 @@ import UIKit extension String { - var localized: String { + public var localized: String { return NSLocalizedString(self, comment: "") } @@ -30,13 +30,20 @@ extension String { return !self.isEmpty && rangeOfCharacter(from: nonDigitsCharacterSet) == nil } - func matches(regExp: String) -> Bool { + var pathRepresentation : String { + if !self.hasSuffix("/") { + return String(format: "%@/", self) + } + return self + } + + public func matches(regExp: String) -> Bool { guard let regex = try? NSRegularExpression(pattern: regExp) else { return false } let range = NSRange(location: 0, length: self.count) return regex.firstMatch(in: self, options: [], range: range) != nil } - func matches(for regex: String) -> [String] { + public func matches(for regex: String) -> [String] { do { let regex = try NSRegularExpression(pattern: regex) let results = regex.matches(in: self, @@ -49,14 +56,14 @@ extension String { } } - func replacingOccurrences(for regex: String) -> String { + public func replacingOccurrences(for regex: String) -> String { if let regex = try? NSRegularExpression(pattern: regex, options: .caseInsensitive) { return regex.stringByReplacingMatches(in: self, options: [], range: NSRange(location: 0, length: self.count), withTemplate: "") } return self } - func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat { + public func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat { let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil) diff --git a/ownCloudAppShared/ownCloudAppShared.h b/ownCloudAppShared/ownCloudAppShared.h new file mode 100644 index 000000000..227543450 --- /dev/null +++ b/ownCloudAppShared/ownCloudAppShared.h @@ -0,0 +1,29 @@ +// +// ownCloudAppShared.h +// ownCloudAppShared +// +// Created by Matthias Hühne on 29.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import + +//! Project version number for ownCloudAppShared. +FOUNDATION_EXPORT double ownCloudAppSharedVersionNumber; + +//! Project version string for ownCloudAppShared. +FOUNDATION_EXPORT const unsigned char ownCloudAppSharedVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/ownCloudAppTests/Info.plist b/ownCloudAppTests/Info.plist index 16016759a..2b2be950c 100644 --- a/ownCloudAppTests/Info.plist +++ b/ownCloudAppTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 141 + 148 diff --git a/ownCloudScreenshotsTests/Info.plist b/ownCloudScreenshotsTests/Info.plist index 26a93fab7..8b1229f4f 100644 --- a/ownCloudScreenshotsTests/Info.plist +++ b/ownCloudScreenshotsTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 144 + 149 diff --git a/ownCloudTests/Info.plist b/ownCloudTests/Info.plist index 26a93fab7..8b1229f4f 100644 --- a/ownCloudTests/Info.plist +++ b/ownCloudTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 144 + 149