Files
Project_Velocity/iOS/velocity-ipad/velocity/Features/Settings/SessionConfigurationPanel.swift
sayan eeb684b46c feat: Ipad app production readiness, Colony orchestration, Social posting (#44)
#38 Ipad app production readiness, Colony orchestration, Social posting

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: sagnik/Project_Velocity#44
2026-05-03 18:30:38 +05:30

268 lines
11 KiB
Swift

import SwiftUI
struct SessionConfigurationPanel: View {
@State private var session = SessionStore.shared
let title: String
let subtitle: String
let primaryActionTitle: String
let allowsClearingStoredConfiguration: Bool
var body: some View {
@Bindable var session = session
VStack(alignment: .leading, spacing: 18) {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Text(subtitle)
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
VStack(spacing: 14) {
SessionInputField(
label: "Backend endpoint",
placeholder: SessionConfigurationDefaults.productionBaseURL
) {
TextField("", text: $session.draftBaseURL, prompt: Text(SessionConfigurationDefaults.productionBaseURL))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
}
SessionInputField(
label: "Dream Weaver endpoint",
placeholder: "Leave blank to use the backend endpoint"
) {
TextField("", text: $session.draftDreamWeaverBaseURL, prompt: Text("https://dreamweaver.desineuron.in"))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
}
SessionInputField(
label: "Dream Weaver gateway API key",
placeholder: session.currentConfiguration.hasDreamWeaverAPIKey ? "Leave blank to keep the current gateway key" : "Optional unless the gateway enforces it"
) {
SecureField(
"",
text: $session.draftDreamWeaverAPIKey,
prompt: Text(session.currentConfiguration.hasDreamWeaverAPIKey ? "Leave blank to keep the current gateway key" : "Optional unless the gateway enforces it")
)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
VStack(alignment: .leading, spacing: 8) {
Text("Authentication mode")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.mutedFg)
Picker("Authentication mode", selection: $session.draftAuthMode) {
ForEach(SessionAuthMode.allCases) { mode in
Text(mode.rawValue).tag(mode)
}
}
.pickerStyle(.segmented)
}
if session.draftAuthMode == .emailPassword {
SessionInputField(
label: "Operator email",
placeholder: "operator@desineuron.in"
) {
TextField("", text: $session.draftEmail, prompt: Text("operator@desineuron.in"))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
}
SessionInputField(
label: "Password",
placeholder: session.currentConfiguration.hasPassword ? "Leave blank to keep the current secret" : "Required"
) {
SecureField(
"",
text: $session.draftPassword,
prompt: Text(session.currentConfiguration.hasPassword ? "Leave blank to keep the current secret" : "Required")
)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
} else {
SessionInputField(
label: "Bearer token",
placeholder: session.currentConfiguration.hasBearerToken ? "Leave blank to keep the current token" : "Required"
) {
SecureField(
"",
text: $session.draftBearerToken,
prompt: Text(session.currentConfiguration.hasBearerToken ? "Leave blank to keep the current token" : "Required")
)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Current source: \(session.configurationSourceDescription)")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.foreground)
Text("Runtime overrides are saved on-device. Secrets are stored in Keychain; the backend endpoint, optional Dream Weaver endpoint, and operator email are stored in local app preferences.")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.mutedFg)
}
if let message = session.statusMessage {
SessionStatusBanner(message: message, accentColor: VelocityTheme.success)
}
if let error = session.errorMessage {
SessionStatusBanner(message: error, accentColor: VelocityTheme.danger)
}
HStack(spacing: 12) {
Button {
Task { await session.saveDraft() }
} label: {
HStack(spacing: 8) {
if session.isSaving {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.tint(.white)
}
Text(primaryActionTitle)
.font(.system(size: 14, weight: .semibold))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.buttonStyle(SessionActionButtonStyle(background: VelocityTheme.accent))
.disabled(session.isSaving)
Button("Reset form") {
session.discardDraftChanges()
}
.buttonStyle(SessionSecondaryButtonStyle())
.disabled(session.isSaving || !session.hasUnsavedChanges)
}
if allowsClearingStoredConfiguration {
Button("Clear stored session override") {
Task { await session.clearStoredConfiguration() }
}
.buttonStyle(SessionDangerButtonStyle())
.disabled(session.isSaving || !session.isUsingStoredRuntimeConfiguration)
}
}
.padding(20)
.glassCard(cornerRadius: 20)
}
}
private struct SessionInputField<Field: View>: View {
let label: String
let placeholder: String
@ViewBuilder let field: Field
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(label)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.mutedFg)
field
.font(.system(size: 14))
.foregroundStyle(VelocityTheme.foreground)
.padding(.horizontal, 14)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(VelocityTheme.surface)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(VelocityTheme.borderSubtle, lineWidth: 1)
)
)
Text(placeholder)
.font(.system(size: 10))
.foregroundStyle(VelocityTheme.mutedFg.opacity(0.9))
}
}
}
private struct SessionStatusBanner: View {
let message: String
let accentColor: Color
var body: some View {
Text(message)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(accentColor)
.padding(.horizontal, 14)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(accentColor.opacity(0.10))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(accentColor.opacity(0.18), lineWidth: 1)
)
)
}
}
private struct SessionActionButtonStyle: ButtonStyle {
let background: Color
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(.white)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(background.opacity(configuration.isPressed ? 0.82 : 1))
)
}
}
private struct SessionSecondaryButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 14, weight: .medium))
.foregroundStyle(VelocityTheme.foreground)
.padding(.horizontal, 18)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(VelocityTheme.surface.opacity(configuration.isPressed ? 0.78 : 1))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(VelocityTheme.borderSubtle, lineWidth: 1)
)
)
}
}
private struct SessionDangerButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 13, weight: .medium))
.foregroundStyle(VelocityTheme.danger)
.padding(.horizontal, 14)
.padding(.vertical, 11)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(VelocityTheme.danger.opacity(configuration.isPressed ? 0.14 : 0.10))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(VelocityTheme.danger.opacity(0.2), lineWidth: 1)
)
)
}
}