- LegalDocumentView: renderiza HTML do bundle usando WebView nativo do WebKit (iOS 26+) com suporte a dark mode - AboutView: tela com identidade visual do app, links para os documentos legais e contato - politicadeprivacidade.html / termosecondicoes.html: adicionados ao bundle com CSS para dark mode - BooksView: botão ⓘ na barra de navegação abre AboutView como sheet - PlanSettingsView: seção "Legal" com links diretos para os documentos Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
6.7 KiB
Swift
184 lines
6.7 KiB
Swift
//
|
|
// 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
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
|
|
@State private var authorizationStatus: UNAuthorizationStatus = .notDetermined
|
|
@State private var showRestartConfirmation = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
remindersSection
|
|
planSection
|
|
legalSection
|
|
}
|
|
.navigationTitle("Plano de Leitura")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("OK") { dismiss() }
|
|
}
|
|
}
|
|
// Re-verifica a permissão sempre que o app volta ao primeiro plano —
|
|
// essencial para refletir mudanças feitas nos Ajustes do iPhone.
|
|
.task(id: scenePhase) {
|
|
guard scenePhase == .active else { return }
|
|
authorizationStatus = await ReadingReminderService.authorizationStatus()
|
|
if authorizationStatus == .authorized, plan.reminderEnabled {
|
|
await ReadingReminderService.refresh(for: plan)
|
|
}
|
|
}
|
|
.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 {
|
|
// Otimista: liga já, para o switch responder ao toque;
|
|
// reverte se a autorização for negada.
|
|
plan.reminderEnabled = true
|
|
Task {
|
|
let status = await ReadingReminderService.authorizationStatus()
|
|
var granted = status == .authorized || status == .provisional
|
|
if status == .notDetermined {
|
|
granted = (try? await ReadingReminderService.requestAuthorization()) ?? false
|
|
}
|
|
if granted {
|
|
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: - Legal
|
|
|
|
private var legalSection: some View {
|
|
Section("Legal") {
|
|
NavigationLink {
|
|
LegalDocumentView(
|
|
title: "Política de Privacidade",
|
|
htmlFileName: "politicadeprivacidade"
|
|
)
|
|
} label: {
|
|
Label("Política de Privacidade", systemImage: "hand.raised.fill")
|
|
}
|
|
NavigationLink {
|
|
LegalDocumentView(
|
|
title: "Termos e Condições",
|
|
htmlFileName: "termosecondicoes"
|
|
)
|
|
} label: {
|
|
Label("Termos e Condições", systemImage: "doc.text.fill")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|