diff --git a/ios/happwn/UI/AddSubscriptionView.swift b/ios/happwn/UI/AddSubscriptionView.swift index 90e9d5e..bc5781f 100644 --- a/ios/happwn/UI/AddSubscriptionView.swift +++ b/ios/happwn/UI/AddSubscriptionView.swift @@ -50,6 +50,7 @@ struct AddSubscriptionView: View { } .navigationTitle("Новая подписка") .navigationBarTitleDisplayMode(.inline) + .keyboardDoneButton() .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Отмена") { dismiss() } diff --git a/ios/happwn/UI/ExtractView.swift b/ios/happwn/UI/ExtractView.swift index 5f28a1a..1ad7879 100644 --- a/ios/happwn/UI/ExtractView.swift +++ b/ios/happwn/UI/ExtractView.swift @@ -34,6 +34,7 @@ struct ExtractView: View { } .background(Color(.systemGroupedBackground)) .navigationTitle("happwn") + .keyboardDoneButton() } private var linkSection: some View { diff --git a/ios/happwn/UI/SettingsView.swift b/ios/happwn/UI/SettingsView.swift index 278dbfe..c6c5abd 100644 --- a/ios/happwn/UI/SettingsView.swift +++ b/ios/happwn/UI/SettingsView.swift @@ -1,7 +1,12 @@ import SwiftUI +import UserNotifications +import UIKit struct SettingsView: View { @EnvironmentObject private var settings: Settings + @Environment(\.openURL) private var openURL + @Environment(\.scenePhase) private var scenePhase + @State private var notifStatus: UNAuthorizationStatus = .notDetermined private let columns = [GridItem(.adaptive(minimum: 44), spacing: 14)] @@ -34,9 +39,33 @@ struct SettingsView: View { Toggle("Уведомления об обновлениях", isOn: $settings.notificationsEnabled) .onChange(of: settings.notificationsEnabled) { enabled in if enabled { - Task { await NotificationService().requestAuthorization() } + Task { + await NotificationService().requestAuthorization() + await refreshNotifStatus() + } + } + } + HStack { + Text("Разрешение").foregroundStyle(.secondary) + Spacer() + Text(notifStatusText) + .foregroundStyle(notifStatus == .authorized ? .green : .secondary) + } + if settings.notificationsEnabled && (notifStatus == .denied || notifStatus == .notDetermined) { + Button { + if notifStatus == .notDetermined { + Task { + await NotificationService().requestAuthorization() + await refreshNotifStatus() + } + } else if let url = URL(string: UIApplication.openSettingsURLString) { + openURL(url) } + } label: { + Label(notifStatus == .notDetermined ? "Запросить разрешение" : "Включить в Настройках iOS", + systemImage: "bell.badge") } + } } header: { Text("Обновления") } footer: { @@ -55,6 +84,25 @@ struct SettingsView: View { } } .navigationTitle("Настройки") + .keyboardDoneButton() + .task { await refreshNotifStatus() } + .onChange(of: scenePhase) { phase in + if phase == .active { Task { await refreshNotifStatus() } } + } + } + + private var notifStatusText: String { + switch notifStatus { + case .authorized, .provisional, .ephemeral: return "Разрешены" + case .denied: return "Запрещены" + case .notDetermined: return "Не запрошены" + @unknown default: return "—" + } + } + + @MainActor + private func refreshNotifStatus() async { + notifStatus = await NotificationService().authorizationStatus() } private func labeledField(icon: String, tint: Color, title: String, diff --git a/ios/happwn/UI/SubscriptionDetailView.swift b/ios/happwn/UI/SubscriptionDetailView.swift index 91e1642..d49d984 100644 --- a/ios/happwn/UI/SubscriptionDetailView.swift +++ b/ios/happwn/UI/SubscriptionDetailView.swift @@ -19,6 +19,7 @@ struct SubscriptionDetailView: View { } .navigationTitle(sub?.name ?? "Подписка") .navigationBarTitleDisplayMode(.inline) + .keyboardDoneButton() .onAppear { store.markSeen(id) } } diff --git a/ios/happwn/UI/Theme.swift b/ios/happwn/UI/Theme.swift index 59c9821..c5cc9e3 100644 --- a/ios/happwn/UI/Theme.swift +++ b/ios/happwn/UI/Theme.swift @@ -178,6 +178,29 @@ struct PrimaryButton: View { } } +// MARK: - Keyboard dismissal + +/// Adds a "Готово" button above the keyboard to dismiss it — needed for +/// multiline fields where Return can't close the keyboard. +struct KeyboardDoneToolbar: ViewModifier { + func body(content: Content) -> some View { + content.toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Готово") { + UIApplication.shared.sendAction( + #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + .fontWeight(.semibold) + } + } + } +} + +extension View { + func keyboardDoneButton() -> some View { modifier(KeyboardDoneToolbar()) } +} + // MARK: - Haptics enum Haptics { diff --git a/ios/project.yml b/ios/project.yml index 85b9ea8..778a1b9 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -8,8 +8,8 @@ settings: GENERATE_INFOPLIST_FILE: YES INFOPLIST_KEY_UILaunchScreen_Generation: YES INFOPLIST_KEY_CFBundleDisplayName: happwn - MARKETING_VERSION: "1.0.3" - CURRENT_PROJECT_VERSION: "4" + MARKETING_VERSION: "1.0.4" + CURRENT_PROJECT_VERSION: "5" SWIFT_VERSION: "5.0" TARGETED_DEVICE_FAMILY: "1,2" targets: