Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hub improvements #403

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion Cryptomator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,12 @@
74F5DC1F26DD036D00AFE989 /* StoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */; };
74FC576125ADED030003ED27 /* VaultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FC576025ADED030003ED27 /* VaultCell.swift */; };
B330CB452CB5735300C21E03 /* UnauthorizedErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */; };
B34C53262D142B1000F30FE9 /* EnterSharePointURLViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */; };
B34C53282D142B5800F30FE9 /* EnterSharePointURLViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */; };
B34C532A2D142BA700F30FE9 /* SharePointURLSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */; };
B34C532C2D142BF600F30FE9 /* URLValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C532B2D142BE000F30FE9 /* URLValidator.swift */; };
B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */; };
B379DBC12D27F5B5003B5849 /* SharePointDriveListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */; };
B3D19A442CB937C700CD18A5 /* FileProviderCoordinatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -1040,6 +1046,13 @@
74F5DC1E26DD036D00AFE989 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = "<group>"; };
74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = "<group>"; };
B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthorizedErrorViewController.swift; sourceTree = "<group>"; };
B34C53212D1355D900F30FE9 /* cloud-access-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "cloud-access-swift"; path = "../cloud-access-swift"; sourceTree = SOURCE_ROOT; };
B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewController.swift; sourceTree = "<group>"; };
B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewModel.swift; sourceTree = "<group>"; };
B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointURLSetting.swift; sourceTree = "<group>"; };
B34C532B2D142BE000F30FE9 /* URLValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLValidator.swift; sourceTree = "<group>"; };
B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointDriveListViewController.swift; sourceTree = "<group>"; };
B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointDriveListViewModel.swift; sourceTree = "<group>"; };
B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCoordinatorError.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -1378,6 +1391,12 @@
4A644B45267A3D21008CBB9A /* CreateNewVault */ = {
isa = PBXGroup;
children = (
B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */,
B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */,
B34C532B2D142BE000F30FE9 /* URLValidator.swift */,
B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */,
B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */,
B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */,
4A644B4A267B4C08008CBB9A /* CreateNewVaultChooseFolderViewController.swift */,
4A53CC16267CDBFF00853BB3 /* CreateNewVaultChooseFolderViewModel.swift */,
4A644B4C267B55E4008CBB9A /* CreateNewVaultCoordinator.swift */,
Expand Down Expand Up @@ -2482,7 +2501,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ -f ./fastlane/scripts/.cloud-access-secrets.sh ]; then\n source ./fastlane/scripts/.cloud-access-secrets.sh \"${CONFIG_NAME}\"\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:3 string ${HUB_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n";
shellScript = "if [ -f ./fastlane/scripts/.cloud-access-secrets.sh ]; then\n source ./fastlane/scripts/.cloud-access-secrets.sh \"${CONFIG_NAME}\"\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${MICROSOFT_GRAPH_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:3 string ${HUB_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n";
};
742595D72552EE0000A8A008 /* Set Build Number */ = {
isa = PBXShellScriptBuildPhase;
Expand Down Expand Up @@ -2718,6 +2737,7 @@
4A6A521D268B7C8F006F7368 /* BaseNavigationController.swift in Sources */,
4AC005F127C3D80B006FFE87 /* PremiumManager.swift in Sources */,
4ADD2342267383BE00374E4E /* AddVaultSuccessViewModel.swift in Sources */,
B34C53262D142B1000F30FE9 /* EnterSharePointURLViewController.swift in Sources */,
4AB1D4F827D68026009060AB /* IAPHeaderView.swift in Sources */,
4A79E26926B16993008C9959 /* ActionButton.swift in Sources */,
4AF91CD925A722A600ACF01E /* VaultInfo.swift in Sources */,
Expand Down Expand Up @@ -2775,6 +2795,7 @@
4A4B7E7426B954D2009BFDB1 /* HeaderFooterViewModel.swift in Sources */,
4A5AC441275A5B3500342AA7 /* PurchaseAlert.swift in Sources */,
74C2BC5026E8FCC100BCAA03 /* PurchaseViewModel.swift in Sources */,
B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */,
4A644B53267BAFDA008CBB9A /* CreateNewFolderViewModel.swift in Sources */,
4AB8539826BA881F00555F00 /* VaultDetailUnlockVaultViewModel.swift in Sources */,
4AF4535F272066A600CF1919 /* RenameVaultViewController.swift in Sources */,
Expand All @@ -2790,6 +2811,7 @@
4AF91D0D25A8D5EF00ACF01E /* ListViewModel.swift in Sources */,
4A8D060525C82F1F0082C5F7 /* AddVaultSuccesing.swift in Sources */,
4A61F6B9274582E3007AA422 /* StaticUITableViewController.swift in Sources */,
B379DBC12D27F5B5003B5849 /* SharePointDriveListViewModel.swift in Sources */,
4A21B49426BC0127000D13DF /* BindableAttributedTextHeaderFooterViewModel.swift in Sources */,
740D367E266A18DF0058744D /* SettingsViewController.swift in Sources */,
4AF45356271F2A8300CF1919 /* RenameVaultViewModel.swift in Sources */,
Expand Down Expand Up @@ -2820,17 +2842,20 @@
4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */,
4A3D658226838991000DA764 /* OpenExistingLocalVaultViewModel.swift in Sources */,
4AC86270273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift in Sources */,
B34C53282D142B5800F30FE9 /* EnterSharePointURLViewModel.swift in Sources */,
4AB1D4FD27D69BB2009060AB /* TrialCell.swift in Sources */,
4A2FD08225B5E2BA008565C8 /* VaultInstalling.swift in Sources */,
74C2BC5226E8FCD000BCAA03 /* PurchaseCoordinator.swift in Sources */,
4A53B6D32722F92D000DC367 /* MoveVaultViewModel.swift in Sources */,
4A4B7E4426B2B1A5009BFDB1 /* BindableTableViewCellViewModel.swift in Sources */,
B34C532C2D142BF600F30FE9 /* URLValidator.swift in Sources */,
4AED9A69286B303000352951 /* S3Authenticator+VC.swift in Sources */,
4ADBD35827284BAB00B19B5C /* MoveVaultViewController.swift in Sources */,
7408E6CD26779BCC00D7FAEA /* AboutViewModel.swift in Sources */,
4A8A6424286CA72B001F5EB9 /* DefaultShowEditAccountBehavior.swift in Sources */,
4AB1D4FF27D69C9A009060AB /* DisclosureCell.swift in Sources */,
4A7077FF278DC2ED00AEF4CE /* VaultKeepUnlockedViewController.swift in Sources */,
B34C532A2D142BA700F30FE9 /* SharePointURLSetting.swift in Sources */,
4A21B49C26BD68C2000D13DF /* UIControl+Publisher.swift in Sources */,
4AF91CD025A71C5800ACF01E /* UIImage+CloudProviderType.swift in Sources */,
4A4B7E4A26B2C071009BFDB1 /* ButtonCellViewModel.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@ import CryptomatorCloudAccessCore
import CryptomatorCommonCore
import UIKit

class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator {
class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator, SharePointURLSetting {
var navigationController: UINavigationController
var childCoordinators = [Coordinator]()
weak var parentCoordinator: Coordinator?

private let vaultName: String
private var currentSharePointAccount: AccountInfo?

init(navigationController: UINavigationController, vaultName: String) {
self.navigationController = navigationController
self.vaultName = vaultName
}

func start() {
let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.createNewVault.chooseCloud.header"))
let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .sharePoint, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.createNewVault.chooseCloud.header"))
let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel)
chooseCloudVC.title = LocalizedString.getValue("addVault.createNewVault.title")
chooseCloudVC.coordinator = self
Expand All @@ -44,15 +45,59 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA

func showAddAccount(for cloudProviderType: CloudProviderType, from viewController: UIViewController) {
let authenticator = CloudAuthenticator(accountManager: CloudProviderAccountDBManager.shared)
authenticator.authenticate(cloudProviderType, from: viewController).then { account in
let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID)
self.startFolderChooser(with: provider, account: account)
authenticator.authenticate(cloudProviderType, from: viewController).then { _ in
}
}

func showEnterSharePointURL(for account: AccountInfo) {
let viewModel = EnterSharePointURLViewModel(account: account)
let enterURLVC = EnterSharePointURLViewController(viewModel: viewModel)
enterURLVC.coordinator = self
navigationController.pushViewController(enterURLVC, animated: true)
}

func selectedAccont(_ account: AccountInfo) throws {
let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID)
startFolderChooser(with: provider, account: account.cloudProviderAccount)
if account.cloudProviderType == .sharePoint {
currentSharePointAccount = account
showEnterSharePointURL(for: account)
} else {
let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID)
startFolderChooser(with: provider, account: account.cloudProviderAccount)
}
}

func setSharePointURL(_ url: String) {
guard let account = currentSharePointAccount else { return }

let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID)
let discovery = MicrosoftGraphDiscovery(credential: credential)

showDriveList(discovery: discovery, sharePointURL: url)
}

private func showDriveList(discovery: MicrosoftGraphDiscovery, sharePointURL: String) {
guard let account = currentSharePointAccount else { return }
let viewModel = SharePointDriveListViewModel(discovery: discovery, sharePointURL: sharePointURL, account: account)
viewModel.didSelectDrive = { [weak self] drive in
self?.handleDriveSelection(drive: drive)
}
let driveListVC = SharePointDriveListViewController(viewModel: viewModel)
navigationController.pushViewController(driveListVC, animated: true)
}

private func handleDriveSelection(drive: MicrosoftGraphDrive) {
guard let account = currentSharePointAccount else {
print("No current SharePoint account available")
return
}
do {
try MicrosoftGraphDriveManager.shared.saveDriveToKeychain(drive, for: account.accountUID)
let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID)
let provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: drive.identifier)
startFolderChooser(with: provider, account: account.cloudProviderAccount)
} catch {
handleError(error, for: navigationController)
}
}

private func startFolderChooser(with provider: CloudProvider, account: CloudProviderAccount) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
//  EnterSharePointURLViewController.swift
//  Cryptomator
//
//  Created by Majid Achhoud on 03.12.24.
//

import Combine
import CryptomatorCommonCore
import UIKit

class EnterSharePointURLViewController: SingleSectionStaticUITableViewController {
weak var coordinator: (SharePointURLSetting & Coordinator)?
private var viewModel: EnterSharePointURLViewModelProtocol
private var lastReturnButtonPressedSubscriber: AnyCancellable?
init(viewModel: EnterSharePointURLViewModelProtocol) {
self.viewModel = viewModel
super.init(viewModel: viewModel)
}

override func viewDidLoad() {
super.viewDidLoad()
let doneButton = UIBarButtonItem(title: LocalizedString.getValue("common.button.next"), style: .done, target: self, action: #selector(nextButtonClicked))
navigationItem.rightBarButtonItem = doneButton
lastReturnButtonPressedSubscriber = viewModel.lastReturnButtonPressed.sink { [weak self] in
self?.lastReturnButtonPressedAction()
}
}

@objc func nextButtonClicked() {
guard let coordinator = coordinator else { return }
do {
let url = try viewModel.getValidatedSharePointURL()
coordinator.setSharePointURL(url)
} catch {
print("Error validating SharePoint URL: \(error)")
coordinator.handleError(error, for: self)
}
}

func lastReturnButtonPressedAction() {
nextButtonClicked()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// EnterSharePointURLViewModel.swift
// Cryptomator
//
// Created by Majid Achhoud on 03.12.24.
// Copyright © 2024 Skymatic GmbH. All rights reserved.
//

import Combine
import CryptomatorCommonCore
import Foundation

protocol EnterSharePointURLViewModelProtocol: SingleSectionTableViewModel, ReturnButtonSupport {
func getValidatedSharePointURL() throws -> String
}

class EnterSharePointURLViewModel: SingleSectionTableViewModel, EnterSharePointURLViewModelProtocol {
let account: AccountInfo
init(account: AccountInfo) {
self.account = account
}

var lastReturnButtonPressed: AnyPublisher<Void, Never> {
return setupReturnButtonSupport(for: [sharePointURLCellViewModel], subscribers: &subscribers)
}

override var cells: [TableViewCellViewModel] {
return [sharePointURLCellViewModel]
}

override var title: String? {
return LocalizedString.getValue("addVault.enterSharePointURL.title")
}

let sharePointURLCellViewModel = TextFieldCellViewModel(
type: .normal,
placeholder: LocalizedString.getValue("addVault.enterSharePointURL.placeholder"),
isInitialFirstResponder: true
)
var trimmedSharePointURL: String {
return sharePointURLCellViewModel.input.value.trimmingCharacters(in: .whitespacesAndNewlines)
}

private lazy var subscribers = Set<AnyCancellable>()
func getValidatedSharePointURL() throws -> String {
guard !trimmedSharePointURL.isEmpty else {
throw EnterSharePointURLViewModelError.emptyURL
}
try URLValidator.validateSharePointURL(urlString: trimmedSharePointURL)
return trimmedSharePointURL
}

override func getHeaderTitle(for section: Int) -> String? {
guard section == 0 else {
return nil
}
return LocalizedString.getValue("addVault.enterSharePointURL.header.title")
}
}

enum EnterSharePointURLViewModelError: LocalizedError {
case emptyURL
case invalidURL
var errorDescription: String? {
switch self {
case .emptyURL:
return LocalizedString.getValue("addVault.enterSharePointURL.error.emptyURL")
case .invalidURL:
return LocalizedString.getValue("addVault.enterSharePointURL.error.invalidURL")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// SharePointDriveListViewController.swift
// Cryptomator
//
// Created by Majid Achhoud
// Copyright © 2024 Skymatic GmbH. All rights reserved.
//

import CryptomatorCloudAccessCore
import CryptomatorCommonCore
import Foundation
import UIKit

class SharePointDriveListViewController: BaseUITableViewController {
private var viewModel: SharePointDriveListViewModel

init(viewModel: SharePointDriveListViewModel) {
self.viewModel = viewModel
super.init()
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CloudCell.self, forCellReuseIdentifier: "SharePointDriveCell")
viewModel.reloadData = { [weak self] in
self?.tableView.reloadData()
}

self.title = LocalizedString.getValue("addVault.selectDrive.navigation.title")
}

// MARK: - UITableViewDataSource

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.drives.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "SharePointDriveCell", for: indexPath) as? CloudCell else {
fatalError("Could not dequeue CloudCell")
}
Comment on lines +44 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace fatalError with proper error handling

Using fatalError for cell dequeue failure is not suitable for production code as it could crash the app. Consider handling this gracefully.

-		guard let cell = tableView.dequeueReusableCell(withIdentifier: "SharePointDriveCell", for: indexPath) as? CloudCell else {
-			fatalError("Could not dequeue CloudCell")
-		}
+		guard let cell = tableView.dequeueReusableCell(withIdentifier: "SharePointDriveCell", for: indexPath) as? CloudCell else {
+			return UITableViewCell() // Fallback to basic cell
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
guard let cell = tableView.dequeueReusableCell(withIdentifier: "SharePointDriveCell", for: indexPath) as? CloudCell else {
fatalError("Could not dequeue CloudCell")
}
guard let cell = tableView.dequeueReusableCell(withIdentifier: "SharePointDriveCell", for: indexPath) as? CloudCell else {
return UITableViewCell() // Fallback to basic cell
}


let drive = viewModel.drives[indexPath.row]
configure(cell, with: drive)

return cell
}

// MARK: - Styling Configuration

private func configure(_ cell: CloudCell, with drive: MicrosoftGraphDrive) {
cell.textLabel?.text = drive.name
cell.imageView?.image = UIImage(systemName: "folder")
}

// MARK: - UITableViewDelegate

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedDrive = viewModel.drives[indexPath.row]
viewModel.selectDrive(selectedDrive)
tableView.deselectRow(at: indexPath, animated: true)
}
}

Loading
Loading