1.6.0 for Android and 1.1.0 for iOS - Finalizing Export and Import
formats :)
This commit is contained in:
@@ -12,6 +12,9 @@ struct SettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
SyncSection()
|
||||
.environmentObject(dataManager.syncService)
|
||||
|
||||
DataManagementSection(
|
||||
activeSheet: $activeSheet
|
||||
)
|
||||
@@ -303,6 +306,361 @@ struct ExportDataView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct SyncSection: View {
|
||||
@EnvironmentObject var syncService: SyncService
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@State private var showingSyncSettings = false
|
||||
@State private var showingDisconnectAlert = false
|
||||
|
||||
var body: some View {
|
||||
Section("Sync") {
|
||||
// Sync Status
|
||||
HStack {
|
||||
Image(
|
||||
systemName: syncService.isConnected
|
||||
? "checkmark.circle.fill"
|
||||
: syncService.isConfigured
|
||||
? "exclamationmark.triangle.fill"
|
||||
: "exclamationmark.circle.fill"
|
||||
)
|
||||
.foregroundColor(
|
||||
syncService.isConnected
|
||||
? .green
|
||||
: syncService.isConfigured
|
||||
? .orange
|
||||
: .red
|
||||
)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Sync Server")
|
||||
.font(.headline)
|
||||
Text(
|
||||
syncService.isConnected
|
||||
? "Connected"
|
||||
: syncService.isConfigured
|
||||
? "Configured - Not tested"
|
||||
: "Not configured"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Configure Server
|
||||
Button(action: {
|
||||
showingSyncSettings = true
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "gear")
|
||||
.foregroundColor(.blue)
|
||||
Text("Configure Server")
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
if syncService.isConfigured {
|
||||
|
||||
// Sync Now - only show if connected
|
||||
if syncService.isConnected {
|
||||
Button(action: {
|
||||
performSync()
|
||||
}) {
|
||||
HStack {
|
||||
if syncService.isSyncing {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
Text("Syncing...")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.foregroundColor(.green)
|
||||
Text("Sync Now")
|
||||
Spacer()
|
||||
if let lastSync = syncService.lastSyncTime {
|
||||
Text(
|
||||
RelativeDateTimeFormatter().localizedString(
|
||||
for: lastSync, relativeTo: Date())
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(syncService.isSyncing)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
// Auto-sync configuration - always visible for testing
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Auto-sync")
|
||||
Text("Sync automatically on app launch and data changes")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Toggle(
|
||||
"",
|
||||
isOn: Binding(
|
||||
get: { syncService.isAutoSyncEnabled },
|
||||
set: { syncService.isAutoSyncEnabled = $0 }
|
||||
)
|
||||
)
|
||||
.disabled(!syncService.isConnected)
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
// Disconnect option - only show if connected
|
||||
if syncService.isConnected {
|
||||
Button(action: {
|
||||
showingDisconnectAlert = true
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "power")
|
||||
.foregroundColor(.orange)
|
||||
Text("Disconnect")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
if let error = syncService.syncError {
|
||||
Text(error)
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
.padding(.leading, 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingSyncSettings) {
|
||||
SyncSettingsView()
|
||||
.environmentObject(syncService)
|
||||
}
|
||||
.alert("Disconnect from Server", isPresented: $showingDisconnectAlert) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Disconnect", role: .destructive) {
|
||||
syncService.disconnect()
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
"This will sign you out but keep your server settings. You'll need to test the connection again to sync."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func performSync() {
|
||||
Task {
|
||||
do {
|
||||
try await syncService.syncWithServer(dataManager: dataManager)
|
||||
} catch {
|
||||
print("Sync failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SyncSettingsView: View {
|
||||
@EnvironmentObject var syncService: SyncService
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var serverURL: String = ""
|
||||
@State private var authToken: String = ""
|
||||
@State private var showingDisconnectAlert = false
|
||||
@State private var isTesting = false
|
||||
@State private var showingTestResult = false
|
||||
@State private var testResultMessage = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Server URL", text: $serverURL)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.placeholder(when: serverURL.isEmpty) {
|
||||
Text("http://your-server:8080")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
TextField("Auth Token", text: $authToken)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.placeholder(when: authToken.isEmpty) {
|
||||
Text("your-secret-token")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Server Configuration")
|
||||
} footer: {
|
||||
Text(
|
||||
"Enter your sync server URL and authentication token. You must test the connection before syncing is available."
|
||||
)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(action: {
|
||||
testConnection()
|
||||
}) {
|
||||
HStack {
|
||||
if isTesting {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
Text("Testing...")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Image(systemName: "network")
|
||||
.foregroundColor(.blue)
|
||||
Text("Test Connection")
|
||||
Spacer()
|
||||
if syncService.isConnected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(
|
||||
isTesting
|
||||
|| serverURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
|| authToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
)
|
||||
.foregroundColor(.primary)
|
||||
} header: {
|
||||
Text("Connection")
|
||||
} footer: {
|
||||
Text("Test the connection to verify your server settings before saving.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Disconnect from Server") {
|
||||
showingDisconnectAlert = true
|
||||
}
|
||||
.foregroundColor(.orange)
|
||||
|
||||
Button("Clear Configuration") {
|
||||
syncService.clearConfiguration()
|
||||
serverURL = ""
|
||||
authToken = ""
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
} footer: {
|
||||
Text(
|
||||
"Disconnect will sign you out but keep settings. Clear Configuration removes all sync settings."
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Sync Settings")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
let newURL = serverURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let newToken = authToken.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Mark as disconnected if settings changed
|
||||
if newURL != syncService.serverURL || newToken != syncService.authToken {
|
||||
syncService.isConnected = false
|
||||
UserDefaults.standard.set(false, forKey: "sync_is_connected")
|
||||
}
|
||||
|
||||
syncService.serverURL = newURL
|
||||
syncService.authToken = newToken
|
||||
dismiss()
|
||||
}
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
serverURL = syncService.serverURL
|
||||
authToken = syncService.authToken
|
||||
}
|
||||
.alert("Disconnect from Server", isPresented: $showingDisconnectAlert) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Disconnect", role: .destructive) {
|
||||
syncService.disconnect()
|
||||
dismiss()
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
"This will sign you out but keep your server settings. You'll need to test the connection again to sync."
|
||||
)
|
||||
}
|
||||
.alert("Connection Test", isPresented: $showingTestResult) {
|
||||
Button("OK") {}
|
||||
} message: {
|
||||
Text(testResultMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private func testConnection() {
|
||||
isTesting = true
|
||||
|
||||
let testURL = serverURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let testToken = authToken.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Store original values in case test fails
|
||||
let originalURL = syncService.serverURL
|
||||
let originalToken = syncService.authToken
|
||||
|
||||
Task {
|
||||
do {
|
||||
// Temporarily set the values for testing
|
||||
syncService.serverURL = testURL
|
||||
syncService.authToken = testToken
|
||||
|
||||
try await syncService.testConnection()
|
||||
|
||||
await MainActor.run {
|
||||
isTesting = false
|
||||
testResultMessage =
|
||||
"Connection successful! You can now save and sync your data."
|
||||
showingTestResult = true
|
||||
}
|
||||
} catch {
|
||||
// Restore original values if test failed
|
||||
syncService.serverURL = originalURL
|
||||
syncService.authToken = originalToken
|
||||
|
||||
await MainActor.run {
|
||||
isTesting = false
|
||||
testResultMessage = "Connection failed: \(error.localizedDescription)"
|
||||
showingTestResult = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removed AutoSyncSettingsView - now using simple toggle in main settings
|
||||
|
||||
extension View {
|
||||
func placeholder<Content: View>(
|
||||
when shouldShow: Bool,
|
||||
alignment: Alignment = .leading,
|
||||
@ViewBuilder placeholder: () -> Content
|
||||
) -> some View {
|
||||
|
||||
ZStack(alignment: alignment) {
|
||||
placeholder().opacity(shouldShow ? 1 : 0)
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImportDataView: View {
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
Reference in New Issue
Block a user