Adiciona Plano Bíblia em 180 Dias com lembrete diário e ícone do app
- Leitor bíblico consumindo a API NBV (bible.falehandix.com.br): lista de livros por testamento e leitura de capítulos - Plano de 180 dias com três trilhas diárias (História, Sabedoria e Profetas, Novo Testamento) geradas deterministicamente sobre o cânon embutido (1.189 capítulos, cobertura testada) - Lembrete diário via notificação local recorrente (UNCalendarNotificationTrigger, identificador fixo, sem duplicatas), com pedido de autorização em contexto, configurações de horário/som, e deep link da notificação para a Leitura de Hoje - Estado do plano em SwiftData: progresso, pausa/retomada, reinício e conclusão preservando preferências do lembrete - 15 testes unitários (Testing) e 2 testes de UI (XCUIAutomation) - Ícone do app a partir do logo biblia.png Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
150
Bible-Week/PlanSettingsView.swift
Normal file
150
Bible-Week/PlanSettingsView.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// PlanSettingsView.swift
|
||||
// Bible-Week
|
||||
//
|
||||
// Configurações do Plano de 180 Dias: lembrete diário (horário, som),
|
||||
// pausar/continuar e reiniciar.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
|
||||
struct PlanSettingsView: View {
|
||||
@Bindable var plan: ReadingPlanState
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var authorizationStatus: UNAuthorizationStatus = .notDetermined
|
||||
@State private var showRestartConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
remindersSection
|
||||
planSection
|
||||
}
|
||||
.navigationTitle("Plano de Leitura")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("OK") { dismiss() }
|
||||
}
|
||||
}
|
||||
.task {
|
||||
authorizationStatus = await ReadingReminderService.authorizationStatus()
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Reiniciar o plano do dia 1? Seu progresso atual será apagado, mas a preferência de lembrete será mantida.",
|
||||
isPresented: $showRestartConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Reiniciar plano", role: .destructive) {
|
||||
plan.restart()
|
||||
Task { await ReadingReminderService.refresh(for: plan) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lembretes
|
||||
|
||||
@ViewBuilder
|
||||
private var remindersSection: some View {
|
||||
Section("Lembretes") {
|
||||
if authorizationStatus == .denied {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Notificações desativadas")
|
||||
.font(.headline)
|
||||
Text("Para receber lembretes de leitura, habilite as notificações nos Ajustes do iPhone.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Button("Abrir Ajustes") {
|
||||
openNotificationSettings()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
} else {
|
||||
Toggle("Lembrete diário", isOn: reminderEnabledBinding)
|
||||
if plan.reminderEnabled {
|
||||
DatePicker(
|
||||
"Horário",
|
||||
selection: reminderTimeBinding,
|
||||
displayedComponents: .hourAndMinute
|
||||
)
|
||||
Toggle("Som", isOn: soundEnabledBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
}
|
||||
|
||||
/// Ligar pede autorização se necessário; desligar cancela o agendamento.
|
||||
private var reminderEnabledBinding: Binding<Bool> {
|
||||
Binding {
|
||||
plan.reminderEnabled
|
||||
} set: { newValue in
|
||||
if newValue {
|
||||
Task {
|
||||
let status = await ReadingReminderService.authorizationStatus()
|
||||
var granted = status == .authorized || status == .provisional
|
||||
if status == .notDetermined {
|
||||
granted = (try? await ReadingReminderService.requestAuthorization()) ?? false
|
||||
}
|
||||
if granted {
|
||||
plan.reminderEnabled = true
|
||||
await ReadingReminderService.refresh(for: plan)
|
||||
} else {
|
||||
plan.reminderEnabled = false
|
||||
authorizationStatus = .denied
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plan.reminderEnabled = false
|
||||
ReadingReminderService.cancelDailyReminder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Alterar o horário substitui o agendamento anterior (mesmo identificador,
|
||||
/// nunca duplica) e persiste o novo valor.
|
||||
private var reminderTimeBinding: Binding<Date> {
|
||||
Binding {
|
||||
Calendar.current.date(
|
||||
bySettingHour: plan.reminderHour,
|
||||
minute: plan.reminderMinute,
|
||||
second: 0,
|
||||
of: .now
|
||||
) ?? .now
|
||||
} set: { newValue in
|
||||
let components = Calendar.current.dateComponents([.hour, .minute], from: newValue)
|
||||
plan.reminderHour = components.hour ?? 20
|
||||
plan.reminderMinute = components.minute ?? 0
|
||||
Task { await ReadingReminderService.refresh(for: plan) }
|
||||
}
|
||||
}
|
||||
|
||||
private var soundEnabledBinding: Binding<Bool> {
|
||||
Binding {
|
||||
plan.reminderSoundEnabled
|
||||
} set: { newValue in
|
||||
plan.reminderSoundEnabled = newValue
|
||||
Task { await ReadingReminderService.refresh(for: plan) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plano
|
||||
|
||||
private var planSection: some View {
|
||||
Section("Plano") {
|
||||
LabeledContent("Progresso", value: "\(plan.completedDaysCount) de \(PlanSchedule.totalDays) dias")
|
||||
Button(plan.isPaused ? "Continuar plano" : "Pausar plano") {
|
||||
plan.isPaused.toggle()
|
||||
// Pausar remove o agendamento sem apagar a preferência;
|
||||
// continuar restaura automaticamente o horário salvo.
|
||||
Task { await ReadingReminderService.refresh(for: plan) }
|
||||
}
|
||||
Button("Reiniciar plano", role: .destructive) {
|
||||
showRestartConfirmation = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user