add astra draft
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import FamilyControls
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct FamilyQuestsApp: App {
|
||||
@StateObject private var model = AppModel()
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView(model: model)
|
||||
.task { await model.synchronize() }
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
if phase == .active { Task { await model.synchronize() } }
|
||||
if phase == .background { model.closeParentSetup() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RootView: View {
|
||||
@ObservedObject var model: AppModel
|
||||
@State private var server = ""
|
||||
@State private var code = ""
|
||||
@State private var configurationCode = ""
|
||||
@State private var replacementCode = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
if model.credentials == nil {
|
||||
Section("Parent: pair this iPhone") {
|
||||
Text("Create a child and a pairing code in the parent web dashboard. A guardian must approve Apple Family Controls.")
|
||||
TextField("https://your-server.example", text: $server)
|
||||
.textInputAutocapitalization(.never).autocorrectionDisabled().keyboardType(.URL)
|
||||
SecureField("Single-use pairing code", text: $code)
|
||||
.textInputAutocapitalization(.never).autocorrectionDisabled()
|
||||
Button("Pair and request guardian approval") {
|
||||
let submitted = code; code = ""
|
||||
Task { await model.pair(server: server, code: submitted) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Section(model.snapshot?.childName ?? "My time") {
|
||||
if let snapshot = model.snapshot {
|
||||
LabeledContent("Daily allowance", value: "\(snapshot.policy.dailyMinutes) minutes")
|
||||
LabeledContent("Bonus earned today", value: "\(snapshot.bonus(at: Date())) minutes")
|
||||
Text("This is your allowance, not a live remaining-time counter. Rewards expire at the end of the family day and never extend bedtime.")
|
||||
.font(.footnote).foregroundStyle(.secondary)
|
||||
}
|
||||
Text(model.status?.detail ?? "Open or sync the app to check configuration")
|
||||
Button("Sync policy and rewards") { Task { await model.synchronize() } }
|
||||
}
|
||||
Section("Maths challenge") {
|
||||
if let challenge = model.challenge {
|
||||
ForEach(challenge.questions) { question in
|
||||
HStack {
|
||||
Text("\(question.prompt) =")
|
||||
TextField("Answer", text: Binding(
|
||||
get: { model.answers[question.id, default: ""] },
|
||||
set: { model.answers[question.id] = $0 }
|
||||
)).keyboardType(.numberPad).multilineTextAlignment(.trailing)
|
||||
.accessibilityLabel("Answer to \(question.prompt)")
|
||||
}
|
||||
}
|
||||
Button("Check answers") { Task { await model.submitChallenge() } }
|
||||
} else {
|
||||
Text("Answer every question correctly to earn bonus app usage. An internet connection is required to verify answers.")
|
||||
Button("Start challenge") { Task { await model.startChallenge() } }
|
||||
.disabled(model.status?.state != "monitoring")
|
||||
}
|
||||
if !model.rewardMessage.isEmpty { Text(model.rewardMessage) }
|
||||
}
|
||||
Section("Parent setup") {
|
||||
Text("App selection requires a fresh configuration code from the parent dashboard. Do not restrict this app, Phone, or other essential communication and school apps.")
|
||||
.font(.footnote)
|
||||
Button("Request guardian Screen Time authorization") { Task { await model.authorize() } }
|
||||
SecureField("Configuration code", text: $configurationCode)
|
||||
.textInputAutocapitalization(.never).autocorrectionDisabled()
|
||||
Button("Authorize app selection") {
|
||||
let value = configurationCode; configurationCode = ""
|
||||
Task { await model.unlockConfiguration(code: value) }
|
||||
}
|
||||
DisclosureGroup("Replace revoked pairing") {
|
||||
Text("The parent must revoke the old pairing and issue a new pairing code. The server address remains fixed.")
|
||||
SecureField("New pairing code", text: $replacementCode)
|
||||
.textInputAutocapitalization(.never).autocorrectionDisabled()
|
||||
Button("Pair again with the same server") {
|
||||
let value = replacementCode; replacementCode = ""
|
||||
Task { await model.pair(server: "", code: value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !model.message.isEmpty {
|
||||
Section("Needs attention") { Text(model.message).foregroundStyle(.red) }
|
||||
}
|
||||
if model.busy { ProgressView("Working…") }
|
||||
Section { Text("Development MVP. DeviceActivity behavior must be tested on this iOS version before relying on it.").font(.footnote) }
|
||||
}
|
||||
.disabled(model.busy)
|
||||
.navigationTitle("Family Quests")
|
||||
.sheet(isPresented: $model.pickerPresented, onDismiss: { model.closeParentSetup() }) {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
Text("Choose individual entertainment apps only. Leave Family Quests and essential apps accessible.")
|
||||
.font(.footnote).padding()
|
||||
FamilyActivityPicker(selection: $model.draftSelection)
|
||||
}
|
||||
.navigationTitle("Parent app selection")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { Button("Cancel") { model.closeParentSetup() } }
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") { Task { await model.saveSelection() } }.disabled(model.busy)
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
if !model.message.isEmpty { Text(model.message).font(.footnote).padding() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user