diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 14ab1b6769..23462ea011 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -110,6 +110,7 @@ F343A4BF2A1E734600DDA874 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; F343A4C02A1E734600DDA874 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; F343A4C12A1E734600DDA874 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; + F351D1A62D0AF25000930F94 /* PHAssetCollection+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F351D1A52D0AF24A00930F94 /* PHAssetCollection+Extension.swift */; }; F359D8672A7D03420023F405 /* NCUtility+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F359D8662A7D03420023F405 /* NCUtility+Exif.swift */; }; F359D8682A7D03420023F405 /* NCUtility+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F359D8662A7D03420023F405 /* NCUtility+Exif.swift */; }; F359D8692A7D03420023F405 /* NCUtility+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F359D8662A7D03420023F405 /* NCUtility+Exif.swift */; }; @@ -149,6 +150,7 @@ F37208C62BAB63F0006B5430 /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = F37208C52BAB63F0006B5430 /* LRUCache */; }; F37208C82BAB63F1006B5430 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = F37208C72BAB63F1006B5430 /* KeychainAccess */; }; F3754A7D2CF87D600009312E /* SetupPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3754A7C2CF87D600009312E /* SetupPasscodeView.swift */; }; + F389C9F52CEE383300049762 /* SelectAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F389C9F42CEE383300049762 /* SelectAlbumView.swift */; }; F38F71252B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */; }; F39170A92CB82024006127BC /* FileAutoRenamer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */; }; F39170AA2CB82024006127BC /* FileAutoRenamer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */; }; @@ -159,6 +161,7 @@ F39170AF2CB82024006127BC /* FileAutoRenamer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */; }; F39298972A3B12CB00509762 /* BaseNCMoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39298962A3B12CB00509762 /* BaseNCMoreCell.swift */; }; F3953BD72A6E87E000EE03F9 /* BaseIntegrationXCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3953BD62A6E87E000EE03F9 /* BaseIntegrationXCTestCase.swift */; }; + F39A1EE22D0AF8A400DAD522 /* Albums.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39A1EE12D0AF8A200DAD522 /* Albums.swift */; }; F3A047972BD2668800658E7B /* NCAssistantEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A0478F2BD2668800658E7B /* NCAssistantEmptyView.swift */; }; F3A047982BD2668800658E7B /* NCAssistantCreateNewTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047912BD2668800658E7B /* NCAssistantCreateNewTask.swift */; }; F3A047992BD2668800658E7B /* NCAssistantTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047932BD2668800658E7B /* NCAssistantTask.swift */; }; @@ -169,6 +172,7 @@ F3BB464D2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F3BB464C2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib */; }; F3BB46522A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB46512A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift */; }; F3BB46542A3A1E9D00461F6E /* CCCellMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB46532A3A1E9D00461F6E /* CCCellMore.swift */; }; + F3CA337D2D0B2B6C00672333 /* AlbumModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CA337C2D0B2B6A00672333 /* AlbumModel.swift */; }; F3CA33842D10726E00672333 /* NCLoginPollModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CA33832D10726E00672333 /* NCLoginPollModel.swift */; }; F3E173B02C9AF637006D177A /* ScreenAwakeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173AF2C9AF637006D177A /* ScreenAwakeManager.swift */; }; F3E173C02C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; @@ -1219,15 +1223,18 @@ F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS12.swift; sourceTree = ""; }; F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAsset+Extension.swift"; sourceTree = ""; }; F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; }; + F351D1A52D0AF24A00930F94 /* PHAssetCollection+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAssetCollection+Extension.swift"; sourceTree = ""; }; F359D8662A7D03420023F405 /* NCUtility+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCUtility+Exif.swift"; sourceTree = ""; }; F36E64F62B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+SelectTabBar.swift"; sourceTree = ""; }; F37208742BAB4AB0006B5430 /* TestConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = ""; }; F37208772BAB4B5D006B5430 /* BaseXCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseXCTestCase.swift; sourceTree = ""; }; F3754A7C2CF87D600009312E /* SetupPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPasscodeView.swift; sourceTree = ""; }; + F389C9F42CEE383300049762 /* SelectAlbumView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectAlbumView.swift; sourceTree = ""; }; F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonSelectTabBar.swift; sourceTree = ""; }; F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileAutoRenamer+Extensions.swift"; sourceTree = ""; }; F39298962A3B12CB00509762 /* BaseNCMoreCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNCMoreCell.swift; sourceTree = ""; }; F3953BD62A6E87E000EE03F9 /* BaseIntegrationXCTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseIntegrationXCTestCase.swift; sourceTree = ""; }; + F39A1EE12D0AF8A200DAD522 /* Albums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Albums.swift; sourceTree = ""; }; F3A0478F2BD2668800658E7B /* NCAssistantEmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantEmptyView.swift; sourceTree = ""; }; F3A047912BD2668800658E7B /* NCAssistantCreateNewTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantCreateNewTask.swift; sourceTree = ""; }; F3A047932BD2668800658E7B /* NCAssistantTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantTask.swift; sourceTree = ""; }; @@ -1237,6 +1244,7 @@ F3BB464C2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCMoreAppSuggestionsCell.xib; sourceTree = ""; }; F3BB46512A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMoreAppSuggestionsCell.swift; sourceTree = ""; }; F3BB46532A3A1E9D00461F6E /* CCCellMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CCCellMore.swift; sourceTree = ""; }; + F3CA337C2D0B2B6A00672333 /* AlbumModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumModel.swift; sourceTree = ""; }; F3CA33832D10726E00672333 /* NCLoginPollModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCLoginPollModel.swift; sourceTree = ""; }; F3E173AF2C9AF637006D177A /* ScreenAwakeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenAwakeManager.swift; sourceTree = ""; }; F3E173BF2C9B1067006D177A /* AwakeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwakeMode.swift; sourceTree = ""; }; @@ -2009,6 +2017,15 @@ path = Common; sourceTree = ""; }; + F389C9F32CEE381E00049762 /* SelectAlbum */ = { + isa = PBXGroup; + children = ( + F3CA337C2D0B2B6A00672333 /* AlbumModel.swift */, + F389C9F42CEE383300049762 /* SelectAlbumView.swift */, + ); + path = SelectAlbum; + sourceTree = ""; + }; F3A0478E2BD2668800658E7B /* Assistant */ = { isa = PBXGroup; children = ( @@ -2395,6 +2412,7 @@ children = ( F768820B2C0DD1E7001CF441 /* Settings */, F76882162C0DD1E7001CF441 /* AutoUpload */, + F389C9F32CEE381E00049762 /* SelectAlbum */, F768821C2C0DD1E7001CF441 /* Display */, F76882052C0DD1E7001CF441 /* Advanced */, F768821F2C0DD1E7001CF441 /* Helpers */, @@ -2449,6 +2467,7 @@ F76882162C0DD1E7001CF441 /* AutoUpload */ = { isa = PBXGroup; children = ( + F39A1EE12D0AF8A200DAD522 /* Albums.swift */, F76882172C0DD1E7001CF441 /* NCAutoUploadModel.swift */, F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */, ); @@ -2651,6 +2670,7 @@ F7A0D14E259229FA008F8A13 /* Extensions */ = { isa = PBXGroup; children = ( + F351D1A52D0AF24A00930F94 /* PHAssetCollection+Extension.swift */, F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */, F7AC1CAF28AB94490032D99F /* Array+Extension.swift */, F7817CF729801A3500FFBC65 /* Data+Extension.swift */, @@ -3493,7 +3513,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = "Marino Faggiana"; TargetAttributes = { 2C33C47E23E2C475005F963B = { @@ -4403,6 +4423,7 @@ F741C2242B6B9FD600E849BB /* NCMediaSelectTabBar.swift in Sources */, F77A697D250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift in Sources */, F7BF9D822934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */, + F351D1A62D0AF25000930F94 /* PHAssetCollection+Extension.swift in Sources */, AF7E504E27A2D8FF00B5E4AF /* UIBarButton+Extension.swift in Sources */, F7A846DE2BB01ACB0024816F /* NCTrashCellProtocol.swift in Sources */, F799DF852C4B7E56003410B5 /* NCSectionHeader.swift in Sources */, @@ -4466,6 +4487,7 @@ AFCE353327E4ED1900FEA6C2 /* UIToolbar+Extension.swift in Sources */, 8491B1CD273BBA82001C8C5B /* UIViewController+Menu.swift in Sources */, F73EF7BF2B02250B0087E6E9 /* NCManageDatabase+GPS.swift in Sources */, + F39A1EE22D0AF8A400DAD522 /* Albums.swift in Sources */, F71F6D072B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F761856C29E98543006EB3B0 /* NCIntroCollectionViewCell.swift in Sources */, F75DD765290ABB25002EB562 /* Intent.intentdefinition in Sources */, @@ -4479,12 +4501,14 @@ F7E98C1627E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */, F7F4F11227ECDC52008676F9 /* UIFont+Extension.swift in Sources */, F76882222C0DD1E7001CF441 /* NCCapabilitiesView.swift in Sources */, + F3CA337D2D0B2B6C00672333 /* AlbumModel.swift in Sources */, AF93471A27E2361E002537EE /* NCShareHeader.swift in Sources */, F7F878AE1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */, F7A7FDDD2C2DBD6200E9A93A /* NCDeepLinkHandler.swift in Sources */, F778231E2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift in Sources */, 3781B9B023DB2B7E006B4B1D /* AppDelegate+Menu.swift in Sources */, F710D1F52405770F00A6033D /* NCViewerPDF.swift in Sources */, + F389C9F52CEE383300049762 /* SelectAlbumView.swift in Sources */, F7B6B70427C4E7FA00A7F6EB /* NCScan+CollectionView.swift in Sources */, F7816EF22C2C3E1F00A52517 /* NCPushNotification.swift in Sources */, F76882342C0DD1E7001CF441 /* NCDisplayView.swift in Sources */, @@ -5397,13 +5421,13 @@ F77B0F9B1D118A16002130FE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/iOSClient.entitlements"; DEVELOPMENT_TEAM = NKUJUXUJ3B; GCC_SYMBOLS_PRIVATE_EXTERN = YES; INFOPLIST_FILE = "$(SRCROOT)/Brand/iOSClient.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 15; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "it.twsweb.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5413,20 +5437,20 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/iOSClient/Nextcloud-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; - _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES; + _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = NO; }; name = Debug; }; F77B0F9C1D118A16002130FE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/iOSClient.entitlements"; DEVELOPMENT_TEAM = NKUJUXUJ3B; GCC_SYMBOLS_PRIVATE_EXTERN = YES; INFOPLIST_FILE = "$(SRCROOT)/Brand/iOSClient.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 15; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "it.twsweb.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5436,7 +5460,7 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/iOSClient/Nextcloud-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; - _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES; + _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = NO; }; name = Release; }; diff --git a/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension UI.xcscheme b/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension UI.xcscheme index c06627e268..313421491e 100644 --- a/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension UI.xcscheme +++ b/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension UI.xcscheme @@ -1,6 +1,6 @@ Void) { + private func uploadAssetsNewAndFull(controller: NCMainTabBarController?, assetCollections: [PHAssetCollection] = [], selector: String, log: String, account: String, completion: @escaping (_ num: Int) -> Void) { guard let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", account)) else { return completion(0) } @@ -89,7 +93,7 @@ class NCAutoUpload: NSObject { let autoUploadPath = self.database.getAccountAutoUploadPath(session: session) var metadatas: [tableMetadata] = [] - self.getCameraRollAssets(controller: controller, selector: selector, alignPhotoLibrary: false, account: account) { assets in + self.getCameraRollAssets(controller: controller, assetCollections: assetCollections, selector: selector, alignPhotoLibrary: false, account: account) { assets in guard let assets, !assets.isEmpty else { NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Automatic upload, no new assets found [" + log + "]") return completion(0) @@ -210,19 +214,20 @@ class NCAutoUpload: NSObject { } } - private func getCameraRollAssets(controller: NCMainTabBarController?, selector: String, alignPhotoLibrary: Bool, account: String, completion: @escaping (_ assets: [PHAsset]?) -> Void) { + private func getCameraRollAssets(controller: NCMainTabBarController?, assetCollections: [PHAssetCollection] = [], selector: String, alignPhotoLibrary: Bool, account: String, completion: @escaping (_ assets: [PHAsset]?) -> Void) { NCAskAuthorization().askAuthorizationPhotoLibrary(controller: controller) { hasPermission in guard hasPermission, let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", account)) else { return completion(nil) } - let assetCollection = PHAssetCollection.fetchAssetCollections(with: PHAssetCollectionType.smartAlbum, subtype: PHAssetCollectionSubtype.smartAlbumUserLibrary, options: nil) - guard let assetCollection = assetCollection.firstObject else { return completion(nil) } + + var newAssets: [PHAsset] = [] + + let fetchOptions = PHFetchOptions() + let predicateImage = NSPredicate(format: "mediaType == %i", PHAssetMediaType.image.rawValue) let predicateVideo = NSPredicate(format: "mediaType == %i", PHAssetMediaType.video.rawValue) var predicate: NSPredicate? - let fetchOptions = PHFetchOptions() - var newAssets: [PHAsset] = [] if alignPhotoLibrary || (tableAccount.autoUploadImage && tableAccount.autoUploadVideo) { predicate = NSCompoundPredicate(orPredicateWithSubpredicates: [predicateImage, predicateVideo]) @@ -235,29 +240,62 @@ class NCAutoUpload: NSObject { } fetchOptions.predicate = predicate - let assets: PHFetchResult = PHAsset.fetchAssets(in: assetCollection, options: fetchOptions) - - if selector == NCGlobal.shared.selectorUploadAutoUpload, - let idAssets = self.database.getPhotoLibraryIdAsset(image: tableAccount.autoUploadImage, video: tableAccount.autoUploadVideo, account: account) { - assets.enumerateObjects { asset, _, _ in - var creationDateString = "" - if let creationDate = asset.creationDate { - creationDateString = String(describing: creationDate) + + if assetCollections.isEmpty { + let assetCollection = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: PHAssetCollectionSubtype.smartAlbumUserLibrary, options: nil) + guard let assetCollection = assetCollection.firstObject else { return completion(nil) } + + let assets: PHFetchResult = PHAsset.fetchAssets(in: assetCollection, options: fetchOptions) + + if selector == NCGlobal.shared.selectorUploadAutoUpload, + let idAssets = self.database.getPhotoLibraryIdAsset(image: tableAccount.autoUploadImage, video: tableAccount.autoUploadVideo, account: account) { + assets.enumerateObjects { asset, _, _ in + var creationDateString = "" + if let creationDate = asset.creationDate { + creationDateString = String(describing: creationDate) + } + let idAsset = account + asset.localIdentifier + creationDateString + if !idAssets.contains(idAsset) { + if (asset.isFavorite && tableAccount.autoUploadFavoritesOnly) || !tableAccount.autoUploadFavoritesOnly { + newAssets.append(asset) + } + } } - let idAsset = account + asset.localIdentifier + creationDateString - if !idAssets.contains(idAsset) { + } else { + assets.enumerateObjects { asset, _, _ in if (asset.isFavorite && tableAccount.autoUploadFavoritesOnly) || !tableAccount.autoUploadFavoritesOnly { newAssets.append(asset) } } } } else { - assets.enumerateObjects { asset, _, _ in - if (asset.isFavorite && tableAccount.autoUploadFavoritesOnly) || !tableAccount.autoUploadFavoritesOnly { - newAssets.append(asset) + for assetCollection in assetCollections { + let assets: PHFetchResult = PHAsset.fetchAssets(in: assetCollection, options: fetchOptions) + + if selector == NCGlobal.shared.selectorUploadAutoUpload, + let idAssets = self.database.getPhotoLibraryIdAsset(image: tableAccount.autoUploadImage, video: tableAccount.autoUploadVideo, account: account) { + assets.enumerateObjects { asset, _, _ in + var creationDateString = "" + if let creationDate = asset.creationDate { + creationDateString = String(describing: creationDate) + } + let idAsset = account + asset.localIdentifier + creationDateString + if !idAssets.contains(idAsset) { + if (asset.isFavorite && tableAccount.autoUploadFavoritesOnly) || !tableAccount.autoUploadFavoritesOnly { + newAssets.append(asset) + } + } + } + } else { + assets.enumerateObjects { asset, _, _ in + if (asset.isFavorite && tableAccount.autoUploadFavoritesOnly) || !tableAccount.autoUploadFavoritesOnly { + newAssets.append(asset) + } + } } } } + completion(newAssets) } } diff --git a/iOSClient/Settings/AutoUpload/Albums.swift b/iOSClient/Settings/AutoUpload/Albums.swift new file mode 100644 index 0000000000..fdef915110 --- /dev/null +++ b/iOSClient/Settings/AutoUpload/Albums.swift @@ -0,0 +1,13 @@ +// +// Albums.swift +// Nextcloud +// +// Created by Milen Pivchev on 12.12.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +import Photos + +class Albums: ObservableObject { + @Published var smartAlbums: [PHAssetCollection] = [] +} diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift index cafc33c728..5a3d63bc4c 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift @@ -31,8 +31,6 @@ import NextcloudKit class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { /// A state variable that indicates whether auto upload is enabled or not @Published var autoUpload: Bool = false - /// A state variable that indicates whether to open NCSelect View or not - @Published var autoUploadFolder: Bool = false /// A state variable that indicates whether auto upload for photos is enabled or not @Published var autoUploadImage: Bool = false /// A state variable that indicates whether auto upload for photos is restricted to Wi-Fi only or not @@ -57,6 +55,7 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { @Published var error: String = "" let database = NCManageDatabase.shared @Published var autoUploadPath = "\(NCManageDatabase.shared.getAccountAutoUploadFileName())" + /// Root View Controller var controller: NCMainTabBarController? /// A variable user for change the auto upload directory @@ -66,6 +65,10 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { NCSession.shared.getSession(controller: controller) } +// var autoUploadAlbumIds: [String] { +// NCKeychain().getAutoUploadAlbumIds(account: controller?.account ?? "") ?? [] +// } + /// Initialization code to set up the ViewModel with the active account init(controller: NCMainTabBarController?) { self.controller = controller @@ -164,10 +167,11 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { } /// Updates the auto-upload full content setting. - func handleAutoUploadFullChange(newValue: Bool) { + func handleAutoUploadChange(newValue: Bool, assetCollections: [PHAssetCollection]) { updateAccountProperty(\.autoUploadFull, value: newValue) + if newValue { - NCAutoUpload.shared.autoUploadFullPhotos(controller: self.controller, log: "Auto upload full", account: session.account) + NCAutoUpload.shared.autoUploadSelectedAlbums(controller: self.controller, assetCollections: assetCollections, log: "Auto upload selected albums", account: session.account) } else { self.database.clearMetadatasUpload(account: session.account) } @@ -214,8 +218,18 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { self.database.setAccountAutoUploadDirectory(path, session: session) } } + onViewAppear() } + + func createAlbumTitle(autoUploadAlbumIds: Set) -> String { + if autoUploadAlbumIds.count == 1 { + let album = PHAssetCollection.allAlbums.first(where: { autoUploadAlbumIds.first == $0.localIdentifier }) + return (album?.assetCollectionSubtype == .smartAlbumUserLibrary) ? NSLocalizedString("_camera_roll_", comment: "") : (album?.localizedTitle ?? "") + } else { + return NSLocalizedString("_multiple_albums_", comment: "") + } + } } /// An enum that represents the granularity of the subfolders for auto upload diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index 642a515d70..6a96d935b8 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -27,38 +27,49 @@ import UIKit /// A view that allows the user to configure the `auto upload settings for Nextcloud` struct NCAutoUploadView: View { - @ObservedObject var model: NCAutoUploadModel + @StateObject var model: NCAutoUploadModel + @StateObject var albumModel: AlbumModel + @State private var showUploadFolder: Bool = false + @State private var showSelectAlbums: Bool = false var body: some View { - Form { - /// Auto Upload - Section(content: { - Toggle(NSLocalizedString("_autoupload_", comment: ""), isOn: $model.autoUpload) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .onChange(of: model.autoUpload) { newValue in - model.handleAutoUploadChange(newValue: newValue) + VStack { + Form { + /// Auto Upload + Section(content: { + Toggle(NSLocalizedString("_autoupload_", comment: ""), isOn: $model.autoUpload) + .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .onChange(of: model.autoUpload) { newValue in + model.handleAutoUploadChange(newValue: newValue) + albumModel.initAlbums() + } + }, footer: { + Text(NSLocalizedString("_autoupload_notice_", comment: "")) + }) + /// If `autoUpload` state will be true, we will animate out the whole `autoUploadOnView` section + if model.autoUpload { + autoUploadOnView + .transition(.slide) + .animation(.easeInOut, value: model.autoUpload) + } + } + .navigationBarTitle(NSLocalizedString("_auto_upload_folder_", comment: "")) + .onAppear { + model.onViewAppear() + } + .alert(model.error, isPresented: $model.showErrorAlert) { + Button(NSLocalizedString("_ok_", comment: ""), role: .cancel) { } + } + .sheet(isPresented: $showUploadFolder) { + SelectView(serverUrl: $model.serverUrl, session: model.session) + .onDisappear { + model.setAutoUploadDirectory(serverUrl: model.serverUrl) } - .font(.system(size: 16)) - }, footer: { - Text(NSLocalizedString("_autoupload_notice_", comment: "")) - }) - /// If `autoUpload` state will be true, we will animate out the whole `autoUploadOnView` section - if model.autoUpload { - autoUploadOnView - .transition(.slide) - .animation(.easeInOut, value: model.autoUpload) } - } - .navigationBarTitle(NSLocalizedString("_auto_upload_folder_", comment: "")) - .defaultViewModifier(model) - .alert(model.error, isPresented: $model.showErrorAlert) { - Button(NSLocalizedString("_ok_", comment: ""), role: .cancel) { } - } - .sheet(isPresented: $model.autoUploadFolder) { - SelectView(serverUrl: $model.serverUrl, session: model.session) - .onDisappear { - model.setAutoUploadDirectory(serverUrl: model.serverUrl) + .sheet(isPresented: $showSelectAlbums) { + SelectAlbumView(model: albumModel) } + .tint(.primary) } } @@ -66,97 +77,121 @@ struct NCAutoUploadView: View { var autoUploadOnView: some View { Section(content: { Button(action: { - model.autoUploadFolder.toggle() + showUploadFolder.toggle() }, label: { HStack { Image(systemName: "folder") .resizable() .scaledToFit() - .font(Font.system(.body).weight(.light)) .frame(width: 25, height: 25) .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) Text(NSLocalizedString("_autoupload_select_folder_", comment: "")) } - .font(.system(size: 16)) }) - .tint(Color(UIColor.label)) }, footer: { Text("\(NSLocalizedString("_autoupload_current_folder_", comment: "")): \(model.returnPath())") }) - /// Auto Upload Photo - Section(content: { - Toggle(NSLocalizedString("_autoupload_photos_", comment: ""), isOn: $model.autoUploadImage) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .onChange(of: model.autoUploadImage) { newValue in - model.handleAutoUploadImageChange(newValue: newValue) - } - .font(.system(size: 16)) - Toggle(NSLocalizedString("_wifi_only_", comment: ""), isOn: $model.autoUploadWWAnPhoto) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .onChange(of: model.autoUploadWWAnPhoto) { newValue in - model.handleAutoUploadWWAnPhotoChange(newValue: newValue) - } - .font(.system(size: 16)) - }) - /// Auto Upload Video - Section(content: { - Toggle(NSLocalizedString("_autoupload_videos_", comment: ""), isOn: $model.autoUploadVideo) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .onChange(of: model.autoUploadVideo) { newValue in - model.handleAutoUploadVideoChange(newValue: newValue) + + Group { + Section(content: { + NavigationLink(destination: SelectAlbumView(model: albumModel)) { + Button(action: { + showSelectAlbums.toggle() + }, label: { + HStack { + Image(systemName: "person.2.crop.square.stack") + .resizable() + .scaledToFit() + .frame(width: 25, height: 25) + .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) + Text(NSLocalizedString("_upload_from_", comment: "")) + Text(NSLocalizedString(model.createAlbumTitle(autoUploadAlbumIds: albumModel.autoUploadAlbumIds), comment: "")) + .frame(maxWidth: .infinity, alignment: .trailing) + } + }) } - .font(.system(size: 16)) - Toggle(NSLocalizedString("_wifi_only_", comment: ""), isOn: $model.autoUploadWWAnVideo) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .onChange(of: model.autoUploadWWAnVideo) { newValue in - model.handleAutoUploadWWAnVideoChange(newValue: newValue) + }) + + /// Auto Upload Photo + Section(content: { + Toggle(NSLocalizedString("_autoupload_photos_", comment: ""), isOn: $model.autoUploadImage) + .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .onChange(of: model.autoUploadImage) { newValue in + model.handleAutoUploadImageChange(newValue: newValue) + } + Toggle(NSLocalizedString("_wifi_only_", comment: ""), isOn: $model.autoUploadWWAnPhoto) + .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .onChange(of: model.autoUploadWWAnPhoto) { newValue in + model.handleAutoUploadWWAnPhotoChange(newValue: newValue) + } + }) + /// Auto Upload Video + Section(content: { + Toggle(NSLocalizedString("_autoupload_videos_", comment: ""), isOn: $model.autoUploadVideo) + .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .onChange(of: model.autoUploadVideo) { newValue in + model.handleAutoUploadVideoChange(newValue: newValue) + } + Toggle(NSLocalizedString("_wifi_only_", comment: ""), isOn: $model.autoUploadWWAnVideo) + .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .onChange(of: model.autoUploadWWAnVideo) { newValue in + model.handleAutoUploadWWAnVideoChange(newValue: newValue) + } + }) + /// Only upload favorites if desired + Section(content: { + Toggle(NSLocalizedString("_autoupload_favorites_", comment: ""), isOn: $model.autoUploadFavoritesOnly) + .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .onChange(of: model.autoUploadFavoritesOnly) { newValue in + model.handleAutoUploadFavoritesOnlyChange(newValue: newValue) + } + }) + /// Auto Upload create subfolder + Section(content: { + Toggle(NSLocalizedString("_autoupload_create_subfolder_", comment: ""), isOn: $model.autoUploadCreateSubfolder) + .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .onChange(of: model.autoUploadCreateSubfolder) { newValue in + model.handleAutoUploadCreateSubfolderChange(newValue: newValue) + } + Picker(NSLocalizedString("_autoupload_subfolder_granularity_", comment: ""), selection: $model.autoUploadSubfolderGranularity) { + Text(NSLocalizedString("_daily_", comment: "")).tag(Granularity.daily) + Text(NSLocalizedString("_monthly_", comment: "")).tag(Granularity.monthly) + Text(NSLocalizedString("_yearly_", comment: "")).tag(Granularity.yearly) } - .font(.system(size: 16)) - }) - /// Only upload favorites if desired - Section(content: { - Toggle(NSLocalizedString("_autoupload_favorites_", comment: ""), isOn: $model.autoUploadFavoritesOnly) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .onChange(of: model.autoUploadFavoritesOnly) { newValue in - model.handleAutoUploadFavoritesOnlyChange(newValue: newValue) + .onChange(of: model.autoUploadSubfolderGranularity) { newValue in + model.handleAutoUploadSubfolderGranularityChange(newValue: newValue) } - .font(.system(size: 16)) - }) - /// Auto Upload create subfolder + }, footer: { + Text(NSLocalizedString("_autoupload_create_subfolder_footer_", comment: "")) + }) + } + .disabled(model.autoUploadFull) + + /// Auto Upload Full Section(content: { - Toggle(NSLocalizedString("_autoupload_create_subfolder_", comment: ""), isOn: $model.autoUploadCreateSubfolder) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .onChange(of: model.autoUploadCreateSubfolder) { newValue in - model.handleAutoUploadCreateSubfolderChange(newValue: newValue) - } - .font(.system(size: 16)) - Picker(NSLocalizedString("_autoupload_subfolder_granularity_", comment: ""), selection: $model.autoUploadSubfolderGranularity) { - Text(NSLocalizedString("_daily_", comment: "")).tag(Granularity.daily) - Text(NSLocalizedString("_monthly_", comment: "")).tag(Granularity.monthly) - Text(NSLocalizedString("_yearly_", comment: "")).tag(Granularity.yearly) + Toggle(isOn: $model.autoUploadFull) { + Text(model.autoUploadFull ? "_stop_autoupload_" : "_start_autoupload_") + .padding(.horizontal, 20) + .padding(.vertical, 10) } - .font(.system(size: 16)) - .onChange(of: model.autoUploadSubfolderGranularity) { newValue in - model.handleAutoUploadSubfolderGranularityChange(newValue: newValue) + .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .onChange(of: model.autoUploadFull) { newValue in + albumModel.populateSelectedAlbums() + model.handleAutoUploadChange(newValue: newValue, assetCollections: albumModel.selectedAlbums) } + .font(.headline) + .toggleStyle(.button) + .buttonStyle(.bordered) }, footer: { - Text(NSLocalizedString("_autoupload_create_subfolder_footer_", comment: "")) - }) - /// Auto Upload Full - Section(content: { - Toggle(NSLocalizedString("_autoupload_fullphotos_", comment: ""), isOn: $model.autoUploadFull) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .onChange(of: model.autoUploadFull) { newValue in - model.handleAutoUploadFullChange(newValue: newValue) - } - .font(.system(size: 16)) - }, footer: { - Text( - NSLocalizedString("_autoupload_fullphotos_footer_", comment: "") + "\n \n") + Text(NSLocalizedString("_autoupload_fullphotos_footer_", comment: "") + "\n \n") + .padding(5) }) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .listRowInsets(EdgeInsets()) + .background(Color(UIColor.systemGroupedBackground)) } } #Preview { - NCAutoUploadView(model: NCAutoUploadModel(controller: nil)) + NCAutoUploadView(model: NCAutoUploadModel(controller: nil), albumModel: AlbumModel(controller: nil)) } diff --git a/iOSClient/Settings/NCKeychain.swift b/iOSClient/Settings/NCKeychain.swift index 91c28a6fbf..aebd100441 100644 --- a/iOSClient/Settings/NCKeychain.swift +++ b/iOSClient/Settings/NCKeychain.swift @@ -609,6 +609,18 @@ import KeychainAccess return (data, password) } + // MARK: - Albums + + func setAutoUploadAlbumIds(account: String, albumIds: [String]) { + let key = "AlbumIds" + account + keychain[key] = albumIds.joined(separator: ",") + } + + func getAutoUploadAlbumIds(account: String) -> [String]? { + let key = "AlbumIds" + account + return try? keychain.get(key)?.components(separatedBy: ",") + } + // MARK: - private func migrate(key: String) { diff --git a/iOSClient/Settings/SelectAlbum/AlbumModel.swift b/iOSClient/Settings/SelectAlbum/AlbumModel.swift new file mode 100644 index 0000000000..66c84ded26 --- /dev/null +++ b/iOSClient/Settings/SelectAlbum/AlbumModel.swift @@ -0,0 +1,112 @@ +// +// AlbumModel.swift +// Nextcloud +// +// Created by Milen Pivchev on 12.12.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +import Photos + +@MainActor class AlbumModel: NSObject, ObservableObject { + @Published var allPhotosCollection: PHAssetCollection? + @Published var smartAlbums: [PHAssetCollection] = [] + @Published var userAlbums: [PHAssetCollection] = [] + @Published var selectedAlbums: [PHAssetCollection] = [] + @Published var controller: NCMainTabBarController? + + var smartAlbumAssetCollections: PHFetchResult! + var userAlbumAssetCollections: PHFetchResult! + + var autoUploadAlbumIds: Set { + getSavedAlbumIds() + } + + init(controller: NCMainTabBarController?) { + self.controller = controller + super.init() + + initAlbums() + } + + func initAlbums() { + smartAlbums.removeAll() + userAlbums.removeAll() + + Task { @MainActor in + smartAlbumAssetCollections = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .any, options: nil) + smartAlbumAssetCollections?.enumerateObjects { [self] collection, _, _ in + if collection.assetCollectionSubtype == .smartAlbumUserLibrary { + allPhotosCollection = collection + } else if collection.assetCount > 0 { + smartAlbums.append(collection) + } + } + + let options = PHFetchOptions() + options.predicate = NSPredicate(format: "estimatedAssetCount > 0") // Only normal albums have an estimated asset count. Smart albums do not and must be calculated manually via .assetCount + userAlbumAssetCollections = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: options) + + userAlbumAssetCollections?.enumerateObjects { [self] collection, _, _ in + userAlbums.append(collection) + } + + if let allPhotosCollection, autoUploadAlbumIds.isEmpty { + setSavedAlbumIds(selectedAlbums: [allPhotosCollection.localIdentifier]) + } + } + } + + func populateSelectedAlbums() { + let savedAlbums = getSavedAlbumIds() + + selectedAlbums = savedAlbums.compactMap { selectedAlbum in + return smartAlbums.first(where: { $0.localIdentifier == selectedAlbum }) ?? userAlbums.first(where: { $0.localIdentifier == selectedAlbum }) + } + } + + func setSavedAlbumIds(selectedAlbums: Set) { + guard let account = controller?.account else { return } + + NCKeychain().setAutoUploadAlbumIds(account: account, albumIds: Array(selectedAlbums)) + } + + func getSavedAlbumIds() -> Set { + guard let account = controller?.account else { return [] } + + let albumIds = NCKeychain().getAutoUploadAlbumIds(account: account) ?? [] + + return Set(albumIds) + } + + deinit { + PHPhotoLibrary.shared().unregisterChangeObserver(self) + } +} + +extension AlbumModel: PHPhotoLibraryChangeObserver { + nonisolated func photoLibraryDidChange(_ changeInstance: PHChange) { + Task { @MainActor in + // Update the cached fetch results, and reload the table sections to match. + if let changeDetails = changeInstance.changeDetails(for: smartAlbumAssetCollections) { + smartAlbumAssetCollections = changeDetails.fetchResultAfterChanges + smartAlbums.removeAll() + smartAlbumAssetCollections?.enumerateObjects { [self] collection, _, _ in + if collection.assetCollectionSubtype == .smartAlbumUserLibrary { + allPhotosCollection = collection + } else if collection.assetCount > 0 { + smartAlbums.append(collection) + } + } + } + + if let changeDetails = changeInstance.changeDetails(for: userAlbumAssetCollections) { + userAlbumAssetCollections = changeDetails.fetchResultAfterChanges + userAlbums.removeAll() + userAlbumAssetCollections?.enumerateObjects { [self] collection, _, _ in + userAlbums.append(collection) + } + } + } + } +} diff --git a/iOSClient/Settings/SelectAlbum/SelectAlbumView.swift b/iOSClient/Settings/SelectAlbum/SelectAlbumView.swift new file mode 100644 index 0000000000..a90441343b --- /dev/null +++ b/iOSClient/Settings/SelectAlbum/SelectAlbumView.swift @@ -0,0 +1,146 @@ +// +// SelectAlbumView.swift +// Nextcloud +// +// Created by Milen Pivchev on 20.11.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +import SwiftUI +import Photos + +struct SelectAlbumView: View { + @ObservedObject var model: AlbumModel + @State private var oldSelectedAlbums = Set() + @State var selectedAlbums = Set() + + var body: some View { + List { + Section { + SelectionButton(album: model.allPhotosCollection, isSmartAlbum: true, customAssetCount: 0, selection: $selectedAlbums) + } + + if !model.smartAlbums.isEmpty { + SmartAlbums(model: model, selectedAlbums: $selectedAlbums) + } + + if !model.userAlbums.isEmpty { + UserAlbums(model: model, selectedAlbums: $selectedAlbums) + } + } + .onChange(of: selectedAlbums) { newValue in + if newValue.count > 1, oldSelectedAlbums.contains(model.allPhotosCollection?.localIdentifier ?? "") { + selectedAlbums.remove(model.allPhotosCollection?.localIdentifier ?? "") + } else if newValue.contains(model.allPhotosCollection?.localIdentifier ?? "") { + selectedAlbums = [model.allPhotosCollection?.localIdentifier ?? ""] + } else if newValue.isEmpty { + selectedAlbums = [model.allPhotosCollection?.localIdentifier ?? ""] + } + + oldSelectedAlbums = newValue + model.setSavedAlbumIds(selectedAlbums: selectedAlbums) + model.populateSelectedAlbums() + } + .onAppear { + selectedAlbums = model.getSavedAlbumIds() + } + } +} + +#Preview { + SelectAlbumView(model: AlbumModel(controller: nil)) +} + +struct SmartAlbums: View { + @ObservedObject var model: AlbumModel + @Binding var selectedAlbums: Set + + var body: some View { + Section(NSLocalizedString("_smart_albums_", comment: "")) { + ForEach(model.smartAlbums, id: \.localIdentifier) { album in + SelectionButton(album: album, isSmartAlbum: true, selection: $selectedAlbums) + } + } + } +} + +struct UserAlbums: View { + @ObservedObject var model: AlbumModel + @Binding var selectedAlbums: Set + + var body: some View { + Section(NSLocalizedString("_albums_", comment: "")) { + ForEach(model.userAlbums, id: \.localIdentifier) { album in + SelectionButton(album: album, isSmartAlbum: false, selection: $selectedAlbums) + } + } + } +} + +struct SelectionButton: View { + let album: PHAssetCollection? + let isSmartAlbum: Bool + var customAssetCount = 0 + @StateObject var loader: ImageLoader = ImageLoader() + @Binding var selection: Set + + var body: some View { + Button(action: { + withAnimation { + guard let album else { return } + + if selection.contains(album.localIdentifier) { + selection.remove(album.localIdentifier) + } else { + selection.insert(album.localIdentifier) + } + } + }) { + HStack { + Image(systemName: selection.contains(album?.localIdentifier ?? "") ? "checkmark.circle.fill" : "circle") + .imageScale(.large) + + Image(uiImage: loader.image ?? UIImage()) + .resizable() + .scaledToFill() + .frame(width: 70, height: 70) + .clipped() + .cornerRadius(8) + + VStack(alignment: .leading) { + Text((album?.assetCollectionSubtype == .smartAlbumUserLibrary) ? NSLocalizedString("_camera_roll_", comment: "") : (album?.localizedTitle ?? "")) + Text(String((isSmartAlbum ? album?.assetCount : album?.estimatedAssetCount) ?? 0)) // Only normal albums have an estimated asset count. Smart albums do not and must be calculated manually via .assetCount + .font(.footnote).foregroundStyle(.secondary) + } + } + } + .foregroundColor(.primary) + .onAppear { + loader.loadImage(from: album, targetSize: .zero) + } + } +} + +@MainActor class ImageLoader: ObservableObject { + @Published var image: UIImage? + + func loadImage(from album: PHAssetCollection?, targetSize: CGSize) { + let fetchOptions = PHFetchOptions() + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + fetchOptions.fetchLimit = 1 + + let assets: PHFetchResult + + if let album { + assets = PHAsset.fetchAssets(in: album, options: fetchOptions) + } else { + assets = PHAsset.fetchAssets(with: fetchOptions) + } + + guard let asset = assets.firstObject else { return } + + PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: nil) { [weak self] image, _ in + self?.image = image + } + } +} diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index c3bc9f676f..706ef7c660 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -46,7 +46,7 @@ struct NCSettingsView: View { /// `Auto Upload` Section Section(content: { NavigationLink(destination: LazyView { - NCAutoUploadView(model: NCAutoUploadModel(controller: model.controller)) + NCAutoUploadView(model: NCAutoUploadModel(controller: model.controller), albumModel: AlbumModel(controller: model.controller)) }) { HStack { Image(systemName: "photo.circle") diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index c8bcfe328e..45d49e49f3 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -405,7 +405,8 @@ "_autoupload_change_location_footer_" = "Change folder used for \"Automatic upload of camera photos\" (if the option is enabled)"; "_autoupload_not_select_home_" = "Select a folder"; "_autoupload_save_album_" = "Copy photo or video into the photo album"; -"_autoupload_fullphotos_" = "Upload the whole camera roll"; +"_start_autoupload_" = "Start auto uploading"; +"_stop_autoupload_" = "Stop auto uploading"; "_autoupload_fullphotos_footer_" = "Adjust the options above before uploading."; "_autoupload_create_subfolder_" = "Use subfolders"; "_autoupload_create_subfolder_footer_" = "Store in subfolders based on year, month or daily."; @@ -1138,3 +1139,9 @@ "_file_name_new_extension_" = "Changing the extension might cause this file to open in a different application."; "_please_rename_file_" = "Please rename the file or folder."; "hidden_file_name_warning" = "Name will result in a hidden file."; + +"_select_autoupload_albums_" = "Select albums"; +"_upload_from_" = "Upload from:"; +"_multiple_albums_" = "Multiple albums"; +"_smart_albums_" = "Smart albums"; +"_albums_" = "Albums";