import SwiftUI struct BrowseView: View { let connection: NASConnection @Binding var selectedPath: String @Environment(\.dismiss) private var dismiss @State private var currentPath: String = "/" @State private var items: [NASItem] = [] @State private var isLoading = false @State private var error: String? = nil @State private var showCreateFolder = false @State private var newFolderName = "" var body: some View { NavigationStack { ZStack { AppTheme.background.ignoresSafeArea() VStack(spacing: 0) { // Path breadcrumb bar HStack(spacing: 6) { Image(systemName: "externaldrive") .font(.system(size: 11, weight: .medium)) .foregroundStyle(AppTheme.inkTertiary) Text(currentPath) .font(.system(size: 12, weight: .regular)) .foregroundStyle(AppTheme.inkSecondary) .lineLimit(1) .truncationMode(.middle) Spacer() } .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 10) .background(AppTheme.surfaceSunken) Rectangle() .fill(AppTheme.separator) .frame(height: 0.5) if isLoading { loadingState } else if let err = error { errorState(err) } else if items.isEmpty { emptyState } else { folderList } } } .navigationTitle("Choose folder") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } .font(AppTheme.body()) .foregroundStyle(AppTheme.inkSecondary) } ToolbarItem(placement: .navigationBarTrailing) { Button { newFolderName = "" showCreateFolder = true } label: { Image(systemName: "folder.badge.plus") .foregroundStyle(AppTheme.inkSecondary) } .disabled(currentPath == "/" && connection.nasProtocol == .smb) } } .safeAreaInset(edge: .bottom) { selectButton } .alert("New folder", isPresented: $showCreateFolder) { TextField("Folder name", text: $newFolderName) Button("Create") { Task { await createFolder() } } Button("Cancel", role: .cancel) {} } } .task { await loadItems(path: currentPath) } .refreshable { await loadItems(path: currentPath) } } // MARK: — Sub-views private var loadingState: some View { VStack { Spacer() VStack(spacing: 12) { ProgressView() .tint(AppTheme.inkTertiary) .scaleEffect(1.1) Text("Loading…") .font(AppTheme.caption()) .foregroundStyle(AppTheme.inkTertiary) } Spacer() } } private func errorState(_ message: String) -> some View { VStack { Spacer() VStack(spacing: 20) { // Icon ZStack { Circle() .fill(AppTheme.surfaceSunken) .frame(width: 64, height: 64) Image(systemName: "wifi.exclamationmark") .font(.system(size: 24, weight: .light)) .foregroundStyle(AppTheme.inkSecondary) } VStack(spacing: 6) { Text("Could not load folder") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(AppTheme.ink) Text(message) .font(AppTheme.caption()) .foregroundStyle(AppTheme.inkTertiary) .multilineTextAlignment(.center) .padding(.horizontal, 40) } Button(action: { Task { await loadItems(path: currentPath) } }) { HStack(spacing: 6) { Image(systemName: "arrow.clockwise") .font(.system(size: 12, weight: .medium)) Text("Try again") .font(.system(size: 13, weight: .medium)) } .foregroundStyle(AppTheme.ink) .padding(.horizontal, 16) .padding(.vertical, 8) .background(AppTheme.surfaceSunken) .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) } .buttonStyle(ScaleButtonStyle()) } Spacer() } } private var emptyState: some View { VStack { Spacer() VStack(spacing: 10) { Image(systemName: "folder") .font(.system(size: 28, weight: .ultraLight)) .foregroundStyle(AppTheme.inkQuaternary) Text("Empty folder") .font(AppTheme.caption()) .foregroundStyle(AppTheme.inkTertiary) } Spacer() } } private var folderList: some View { List { if currentPath != "/" { Button(action: navigateUp) { HStack(spacing: 10) { Image(systemName: "arrow.up") .font(.system(size: 13, weight: .medium)) .foregroundStyle(AppTheme.inkSecondary) .frame(width: 22) Text("Parent folder") .font(AppTheme.body()) .foregroundStyle(AppTheme.inkSecondary) Spacer() } .padding(.horizontal, 16) .padding(.vertical, 13) } .buttonStyle(ScaleButtonStyle()) .listRowInsets(EdgeInsets()) .listRowSeparator(.hidden) .listRowBackground(Color.clear) } ForEach(items) { item in FolderRow( item: item, isSelected: selectedPath == item.path ) { if item.isDirectory { navigate(to: item.path) } else { selectedPath = item.path } } .contextMenu { if item.isDirectory { Button { navigate(to: item.path) } label: { Label("Open Folder", systemImage: "folder") } Button { selectedPath = item.path } label: { Label("Select This Folder", systemImage: "checkmark.circle") } } else { Button { // Placeholder: Make available offline } label: { Label("Make Available Offline", systemImage: "arrow.down.circle") } } Button(role: .destructive) { // Placeholder: context-sensitive action } label: { Label("More Info", systemImage: "info.circle") } } .listRowInsets(EdgeInsets()) .listRowSeparator(.hidden) .listRowBackground(Color.clear) } } .listStyle(.plain) .background(AppTheme.background) } private var selectButton: some View { VStack(spacing: 0) { Rectangle() .fill(AppTheme.separator) .frame(height: 0.5) HStack(spacing: 12) { // Current path chip HStack(spacing: 5) { Image(systemName: "folder.fill") .font(.system(size: 11, weight: .medium)) .foregroundStyle(selectedPath == currentPath ? AppTheme.interactive : AppTheme.inkQuaternary) Text(currentPath == "/" ? "Root" : (currentPath as NSString).lastPathComponent) .font(.system(size: 12, weight: .medium)) .foregroundStyle(selectedPath == currentPath ? AppTheme.interactive : AppTheme.inkTertiary) .lineLimit(1) } .padding(.horizontal, 10) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 7, style: .continuous) .fill(selectedPath == currentPath ? AppTheme.interactive.opacity(0.1) : AppTheme.surfaceSunken) ) Spacer() Button(action: { selectedPath = currentPath dismiss() }) { Text("Select") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(AppTheme.inkInverse) .padding(.horizontal, 20) .padding(.vertical, 10) .background(currentPath == "/" && connection.nasProtocol == .smb ? AppTheme.inkQuaternary : AppTheme.ink) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) } .buttonStyle(ScaleButtonStyle()) .disabled(currentPath == "/" && connection.nasProtocol == .smb) } .padding(.horizontal, AppTheme.hPad) .padding(.vertical, 12) .background(AppTheme.background) } } // MARK: — Data private func loadItems(path: String) async { isLoading = true error = nil items = [] do { let s: any NASTransferProtocol = connection.nasProtocol == .smb ? SMBService() : SFTPService() try await s.connect(to: connection.host, port: connection.port, username: connection.username, password: connection.password) if path == "/" && connection.nasProtocol == .smb { let shareNames = try await s.listShares() items = shareNames.map { name in NASItem(name: name, path: "/\(name)", isDirectory: true, size: 0, modifiedDate: nil) } } else { items = try await s.listDirectory(at: path).filter { $0.isDirectory } } s.disconnect() } catch let e as BackupError { self.error = e.errorDescription } catch { self.error = error.localizedDescription } isLoading = false } private func navigate(to path: String) { currentPath = path Task { await loadItems(path: path) } } private func navigateUp() { let parent = (currentPath as NSString).deletingLastPathComponent navigate(to: parent.isEmpty ? "/" : parent) } private func createFolder() async { guard !newFolderName.isEmpty else { return } let newPath = currentPath == "/" ? "/\(newFolderName)" : "\(currentPath)/\(newFolderName)" do { let s: any NASTransferProtocol = connection.nasProtocol == .smb ? SMBService() : SFTPService() try await s.connect(to: connection.host, port: connection.port, username: connection.username, password: connection.password) try await s.createDirectory(at: newPath) s.disconnect() await loadItems(path: currentPath) } catch {} } }