Full project scaffold: XcodeGen project.yml, Core models/protocols/errors, SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2), PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager, all feature UIs (Login, Connect, Browse, Backup, History, Settings), Shared components and theme. Builds clean with zero errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
7.2 KiB
Swift
158 lines
7.2 KiB
Swift
import SwiftUI
|
|
|
|
struct ConnectView: View {
|
|
@StateObject private var vm = ConnectViewModel()
|
|
@State private var navigateToDashboard = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ZStack {
|
|
Color.white.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 0) {
|
|
// Logo
|
|
Image("nas_logo_clean")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 56, height: 56)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.padding(.top, 48)
|
|
.padding(.bottom, 28)
|
|
|
|
// Protocol picker
|
|
Picker("Protocol", selection: $vm.selectedProtocol) {
|
|
ForEach(NASProtocol.allCases, id: \.self) { Text($0.rawValue).tag($0) }
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.padding(.horizontal, 20)
|
|
.padding(.bottom, 20)
|
|
|
|
// Form card
|
|
VStack(spacing: 0) {
|
|
fieldRow(icon: "network", placeholder: "IP Address or hostname",
|
|
text: $vm.host, keyboardType: .URL)
|
|
Divider().padding(.leading, 52)
|
|
fieldRow(icon: "person", placeholder: "Username",
|
|
text: $vm.username, keyboardType: .emailAddress)
|
|
Divider().padding(.leading, 52)
|
|
secureFieldRow(icon: "lock", placeholder: "Password", text: $vm.password)
|
|
Divider().padding(.leading, 52)
|
|
|
|
// Folder picker row
|
|
Button(action: { if vm.isConnected { vm.showFolderBrowser = true } }) {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: "folder")
|
|
.font(.system(size: 16))
|
|
.foregroundColor(vm.isConnected ? AppTheme.blue : AppTheme.textTertiary)
|
|
.frame(width: 28)
|
|
Text(vm.remotePath == "/" ? "Destination folder" : vm.remotePath)
|
|
.font(AppTheme.bodyFont)
|
|
.foregroundColor(vm.remotePath == "/" ? AppTheme.textTertiary : AppTheme.textPrimary)
|
|
Spacer()
|
|
Image(systemName: "chevron.right")
|
|
.font(.system(size: 13))
|
|
.foregroundColor(AppTheme.textTertiary)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 14)
|
|
}
|
|
}
|
|
.background(Color.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
|
|
.stroke(vm.isConnected ? AppTheme.connectedBorder :
|
|
(vm.verifyError != nil ? AppTheme.red : AppTheme.cardBorder),
|
|
lineWidth: vm.isConnected ? 1.5 : 1)
|
|
)
|
|
.padding(.horizontal, 20)
|
|
|
|
if let error = vm.verifyError {
|
|
Text(error)
|
|
.font(AppTheme.captionFont)
|
|
.foregroundColor(AppTheme.red)
|
|
.padding(.top, 10)
|
|
.padding(.horizontal, 24)
|
|
}
|
|
|
|
if vm.isConnected {
|
|
Text("Connected successfully")
|
|
.font(AppTheme.captionFont)
|
|
.foregroundColor(AppTheme.green)
|
|
.padding(.top, 10)
|
|
}
|
|
|
|
// Connect button — sits BELOW the card with explicit gap
|
|
Spacer().frame(height: 24)
|
|
|
|
Button(action: {
|
|
if vm.isConnected {
|
|
navigateToDashboard = true
|
|
} else {
|
|
Task { await vm.verify() }
|
|
}
|
|
}) {
|
|
Group {
|
|
if vm.isVerifying {
|
|
ProgressView().tint(.white)
|
|
} else {
|
|
Text(vm.isConnected ? "Connected — continue" : "Connect")
|
|
.font(.system(size: 17, weight: .semibold))
|
|
}
|
|
}
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 52)
|
|
.background(vm.isConnected ? AppTheme.green : AppTheme.blue)
|
|
.clipShape(RoundedRectangle(cornerRadius: 14))
|
|
}
|
|
.disabled(!vm.canConnect)
|
|
.padding(.horizontal, 20)
|
|
.padding(.bottom, 40)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Connect to NAS")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationDestination(isPresented: $navigateToDashboard) {
|
|
MainTabView()
|
|
}
|
|
.sheet(isPresented: $vm.showFolderBrowser) {
|
|
if let conn = ConnectionStore.shared.savedConnection {
|
|
BrowseView(connection: conn, selectedPath: $vm.remotePath)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func fieldRow(icon: String, placeholder: String, text: Binding<String>, keyboardType: UIKeyboardType = .default) -> some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 16))
|
|
.foregroundColor(AppTheme.textSecondary)
|
|
.frame(width: 28)
|
|
TextField(placeholder, text: text)
|
|
.font(AppTheme.bodyFont)
|
|
.keyboardType(keyboardType)
|
|
.autocorrectionDisabled()
|
|
.textInputAutocapitalization(.never)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 14)
|
|
}
|
|
|
|
private func secureFieldRow(icon: String, placeholder: String, text: Binding<String>) -> some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 16))
|
|
.foregroundColor(AppTheme.textSecondary)
|
|
.frame(width: 28)
|
|
SecureField(placeholder, text: text)
|
|
.font(AppTheme.bodyFont)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 14)
|
|
}
|
|
}
|