поддержка увед ураа
This commit is contained in:
parent
22960a3911
commit
e27e343e53
@ -10,9 +10,22 @@
|
|||||||
1D8926E92E43CE4C00C5590A /* MobileMkch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MobileMkch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
1D8926E92E43CE4C00C5590A /* MobileMkch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MobileMkch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
1DBE8B722E4414C80052ED1B /* Exceptions for "MobileMkch" folder in "MobileMkch" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Info.plist,
|
||||||
|
);
|
||||||
|
target = 1D8926E82E43CE4C00C5590A /* MobileMkch */;
|
||||||
|
};
|
||||||
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
1D8926EB2E43CE4C00C5590A /* MobileMkch */ = {
|
1D8926EB2E43CE4C00C5590A /* MobileMkch */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
1DBE8B722E4414C80052ED1B /* Exceptions for "MobileMkch" folder in "MobileMkch" target */,
|
||||||
|
);
|
||||||
path = MobileMkch;
|
path = MobileMkch;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -255,6 +268,7 @@
|
|||||||
DEVELOPMENT_TEAM = 9U88M9D595;
|
DEVELOPMENT_TEAM = 9U88M9D595;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = MobileMkch/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = MobileMkch;
|
INFOPLIST_KEY_CFBundleDisplayName = MobileMkch;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
@ -290,6 +304,7 @@
|
|||||||
DEVELOPMENT_TEAM = 9U88M9D595;
|
DEVELOPMENT_TEAM = 9U88M9D595;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = MobileMkch/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = MobileMkch;
|
INFOPLIST_KEY_CFBundleDisplayName = MobileMkch;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
|
|||||||
@ -509,4 +509,36 @@ class APIClient: ObservableObject {
|
|||||||
|
|
||||||
return String(html[range])
|
return String(html[range])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkNewThreads(forBoard boardCode: String, lastKnownThreadId: Int, completion: @escaping (Result<[Thread], Error>) -> Void) {
|
||||||
|
let url = URL(string: "\(apiURL)/board/\(boardCode)")!
|
||||||
|
|
||||||
|
session.dataTask(with: url) { data, response, error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let error = error {
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
|
httpResponse.statusCode == 200 else {
|
||||||
|
completion(.failure(APIError(message: "Ошибка получения тредов", code: 0)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = data else {
|
||||||
|
completion(.failure(APIError(message: "Нет данных", code: 0)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let threads = try JSONDecoder().decode([Thread].self, from: data)
|
||||||
|
let newThreads = threads.filter { $0.id > lastKnownThreadId }
|
||||||
|
completion(.success(newThreads))
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
80
MobileMkch/BackgroundTaskManager.swift
Normal file
80
MobileMkch/BackgroundTaskManager.swift
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import Foundation
|
||||||
|
import BackgroundTasks
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class BackgroundTaskManager {
|
||||||
|
static let shared = BackgroundTaskManager()
|
||||||
|
|
||||||
|
private var backgroundTaskIdentifier: String {
|
||||||
|
if let savedIdentifier = UserDefaults.standard.string(forKey: "BackgroundTaskIdentifier") {
|
||||||
|
return savedIdentifier
|
||||||
|
}
|
||||||
|
return "com.mkch.MobileMkch.backgroundrefresh"
|
||||||
|
}
|
||||||
|
private let notificationManager = NotificationManager.shared
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func registerBackgroundTasks() {
|
||||||
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in
|
||||||
|
self.handleBackgroundTask(task as! BGAppRefreshTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleBackgroundTask() {
|
||||||
|
let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier)
|
||||||
|
let settings = Settings()
|
||||||
|
let interval = TimeInterval(settings.notificationInterval)
|
||||||
|
request.earliestBeginDate = Date(timeIntervalSinceNow: interval)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try BGTaskScheduler.shared.submit(request)
|
||||||
|
} catch {
|
||||||
|
print("Не удалось запланировать фоновую задачу: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleBackgroundTask(_ task: BGAppRefreshTask) {
|
||||||
|
task.expirationHandler = {
|
||||||
|
task.setTaskCompleted(success: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = Settings()
|
||||||
|
guard settings.notificationsEnabled else {
|
||||||
|
task.setTaskCompleted(success: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
var hasNewThreads = false
|
||||||
|
|
||||||
|
for boardCode in notificationManager.subscribedBoards {
|
||||||
|
group.enter()
|
||||||
|
|
||||||
|
let lastKnownId = UserDefaults.standard.integer(forKey: "lastThreadId_\(boardCode)")
|
||||||
|
|
||||||
|
APIClient().checkNewThreads(forBoard: boardCode, lastKnownThreadId: lastKnownId) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let newThreads):
|
||||||
|
if !newThreads.isEmpty {
|
||||||
|
hasNewThreads = true
|
||||||
|
for thread in newThreads {
|
||||||
|
self.notificationManager.scheduleNotification(for: thread, boardCode: boardCode)
|
||||||
|
}
|
||||||
|
UserDefaults.standard.set(newThreads.first?.id ?? lastKnownId, forKey: "lastThreadId_\(boardCode)")
|
||||||
|
}
|
||||||
|
case .failure:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
task.setTaskCompleted(success: true)
|
||||||
|
if hasNewThreads {
|
||||||
|
self.scheduleBackgroundTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,7 +35,8 @@ struct BoardsView: View {
|
|||||||
ForEach(boards) { board in
|
ForEach(boards) { board in
|
||||||
NavigationLink(destination: ThreadsView(board: board)
|
NavigationLink(destination: ThreadsView(board: board)
|
||||||
.environmentObject(settings)
|
.environmentObject(settings)
|
||||||
.environmentObject(apiClient)) {
|
.environmentObject(apiClient)
|
||||||
|
.environmentObject(NotificationManager.shared)) {
|
||||||
BoardRow(board: board)
|
BoardRow(board: board)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
MobileMkch/Info.plist
Normal file
15
MobileMkch/Info.plist
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>com.mkch.MobileMkch.backgroundrefresh</string>
|
||||||
|
</array>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>background-processing</string>
|
||||||
|
<string>background-fetch</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -12,6 +12,15 @@ struct MobileMkchApp: App {
|
|||||||
@StateObject private var settings = Settings()
|
@StateObject private var settings = Settings()
|
||||||
@StateObject private var apiClient = APIClient()
|
@StateObject private var apiClient = APIClient()
|
||||||
@StateObject private var crashHandler = CrashHandler.shared
|
@StateObject private var crashHandler = CrashHandler.shared
|
||||||
|
@StateObject private var notificationManager = NotificationManager.shared
|
||||||
|
|
||||||
|
private func setupBackgroundTasks() {
|
||||||
|
if let bundleIdentifier = Bundle.main.bundleIdentifier {
|
||||||
|
let backgroundTaskIdentifier = "\(bundleIdentifier).backgroundrefresh"
|
||||||
|
UserDefaults.standard.set(backgroundTaskIdentifier, forKey: "BackgroundTaskIdentifier")
|
||||||
|
print("Background task identifier: \(backgroundTaskIdentifier)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
@ -23,10 +32,15 @@ struct MobileMkchApp: App {
|
|||||||
BoardsView()
|
BoardsView()
|
||||||
.environmentObject(settings)
|
.environmentObject(settings)
|
||||||
.environmentObject(apiClient)
|
.environmentObject(apiClient)
|
||||||
|
.environmentObject(notificationManager)
|
||||||
}
|
}
|
||||||
.preferredColorScheme(settings.theme == "dark" ? .dark : .light)
|
.preferredColorScheme(settings.theme == "dark" ? .dark : .light)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
BackgroundTaskManager.shared.registerBackgroundTasks()
|
||||||
|
setupBackgroundTasks()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
MobileMkch/NotificationManager.swift
Normal file
80
MobileMkch/NotificationManager.swift
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class NotificationManager: ObservableObject {
|
||||||
|
static let shared = NotificationManager()
|
||||||
|
|
||||||
|
@Published var isNotificationsEnabled = false
|
||||||
|
@Published var subscribedBoards: Set<String> = []
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
checkNotificationStatus()
|
||||||
|
loadSubscribedBoards()
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestPermission(completion: @escaping (Bool) -> Void) {
|
||||||
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isNotificationsEnabled = granted
|
||||||
|
completion(granted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNotificationStatus() {
|
||||||
|
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isNotificationsEnabled = settings.authorizationStatus == .authorized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscribeToBoard(_ boardCode: String) {
|
||||||
|
subscribedBoards.insert(boardCode)
|
||||||
|
saveSubscribedBoards()
|
||||||
|
BackgroundTaskManager.shared.scheduleBackgroundTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribeFromBoard(_ boardCode: String) {
|
||||||
|
subscribedBoards.remove(boardCode)
|
||||||
|
saveSubscribedBoards()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleNotification(for thread: Thread, boardCode: String) {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "Новый тред"
|
||||||
|
content.body = "\(thread.title) в /\(boardCode)/"
|
||||||
|
content.sound = .default
|
||||||
|
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
|
||||||
|
let request = UNNotificationRequest(identifier: "thread_\(thread.id)", content: content, trigger: trigger)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleTestNotification() {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "Тестовое уведомление"
|
||||||
|
content.body = "Новый тред: Тестовый тред в /test/"
|
||||||
|
content.sound = .default
|
||||||
|
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
|
||||||
|
let request = UNNotificationRequest(identifier: "test_notification", content: content, trigger: trigger)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadSubscribedBoards() {
|
||||||
|
if let data = UserDefaults.standard.data(forKey: "subscribedBoards"),
|
||||||
|
let boards = try? JSONDecoder().decode(Set<String>.self, from: data) {
|
||||||
|
subscribedBoards = boards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveSubscribedBoards() {
|
||||||
|
if let data = try? JSONEncoder().encode(subscribedBoards) {
|
||||||
|
UserDefaults.standard.set(data, forKey: "subscribedBoards")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
MobileMkch/NotificationSettingsView.swift
Normal file
123
MobileMkch/NotificationSettingsView.swift
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NotificationSettingsView: View {
|
||||||
|
@EnvironmentObject var settings: Settings
|
||||||
|
@EnvironmentObject var notificationManager: NotificationManager
|
||||||
|
@EnvironmentObject var apiClient: APIClient
|
||||||
|
@State private var showingPermissionAlert = false
|
||||||
|
@State private var boards: [Board] = []
|
||||||
|
@State private var isLoadingBoards = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section(header: Text("Уведомления")) {
|
||||||
|
Toggle("Включить уведомления", isOn: $settings.notificationsEnabled)
|
||||||
|
.onChange(of: settings.notificationsEnabled) { newValue in
|
||||||
|
if newValue {
|
||||||
|
requestNotificationPermission()
|
||||||
|
}
|
||||||
|
settings.saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.notificationsEnabled {
|
||||||
|
HStack {
|
||||||
|
Text("Интервал проверки")
|
||||||
|
Spacer()
|
||||||
|
Picker("", selection: $settings.notificationInterval) {
|
||||||
|
Text("5 мин").tag(300)
|
||||||
|
Text("15 мин").tag(900)
|
||||||
|
Text("30 мин").tag(1800)
|
||||||
|
Text("1 час").tag(3600)
|
||||||
|
}
|
||||||
|
.pickerStyle(MenuPickerStyle())
|
||||||
|
}
|
||||||
|
.onChange(of: settings.notificationInterval) { _ in
|
||||||
|
settings.saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.notificationsEnabled {
|
||||||
|
Section(header: Text("Подписки на доски")) {
|
||||||
|
if isLoadingBoards {
|
||||||
|
HStack {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
Text("Загрузка досок...")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ForEach(boards) { board in
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("/\(board.code)/")
|
||||||
|
.font(.headline)
|
||||||
|
Text(board.description.isEmpty ? "Без описания" : board.description)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Toggle("", isOn: Binding(
|
||||||
|
get: { notificationManager.subscribedBoards.contains(board.code) },
|
||||||
|
set: { isSubscribed in
|
||||||
|
if isSubscribed {
|
||||||
|
notificationManager.subscribeToBoard(board.code)
|
||||||
|
} else {
|
||||||
|
notificationManager.unsubscribeFromBoard(board.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Уведомления")
|
||||||
|
.onAppear {
|
||||||
|
if boards.isEmpty {
|
||||||
|
loadBoards()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Разрешить уведомления", isPresented: $showingPermissionAlert) {
|
||||||
|
Button("Настройки") {
|
||||||
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Отмена", role: .cancel) {
|
||||||
|
settings.notificationsEnabled = false
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("Для получения уведомлений о новых тредах необходимо разрешить уведомления в настройках")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadBoards() {
|
||||||
|
isLoadingBoards = true
|
||||||
|
|
||||||
|
apiClient.getBoards { result in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isLoadingBoards = false
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let loadedBoards):
|
||||||
|
self.boards = loadedBoards
|
||||||
|
case .failure:
|
||||||
|
self.boards = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestNotificationPermission() {
|
||||||
|
notificationManager.requestPermission { granted in
|
||||||
|
if !granted {
|
||||||
|
showingPermissionAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,8 @@ class Settings: ObservableObject {
|
|||||||
@Published var pageSize: Int = 10
|
@Published var pageSize: Int = 10
|
||||||
@Published var passcode: String = ""
|
@Published var passcode: String = ""
|
||||||
@Published var key: String = ""
|
@Published var key: String = ""
|
||||||
|
@Published var notificationsEnabled: Bool = false
|
||||||
|
@Published var notificationInterval: Int = 300
|
||||||
|
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
private let settingsKey = "MobileMkchSettings"
|
private let settingsKey = "MobileMkchSettings"
|
||||||
@ -28,6 +30,8 @@ class Settings: ObservableObject {
|
|||||||
self.pageSize = settings.pageSize
|
self.pageSize = settings.pageSize
|
||||||
self.passcode = settings.passcode
|
self.passcode = settings.passcode
|
||||||
self.key = settings.key
|
self.key = settings.key
|
||||||
|
self.notificationsEnabled = settings.notificationsEnabled
|
||||||
|
self.notificationInterval = settings.notificationInterval
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +44,9 @@ class Settings: ObservableObject {
|
|||||||
compactMode: compactMode,
|
compactMode: compactMode,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
passcode: passcode,
|
passcode: passcode,
|
||||||
key: key
|
key: key,
|
||||||
|
notificationsEnabled: notificationsEnabled,
|
||||||
|
notificationInterval: notificationInterval
|
||||||
)
|
)
|
||||||
|
|
||||||
if let data = try? JSONEncoder().encode(settingsData) {
|
if let data = try? JSONEncoder().encode(settingsData) {
|
||||||
@ -57,6 +63,8 @@ class Settings: ObservableObject {
|
|||||||
pageSize = 10
|
pageSize = 10
|
||||||
passcode = ""
|
passcode = ""
|
||||||
key = ""
|
key = ""
|
||||||
|
notificationsEnabled = false
|
||||||
|
notificationInterval = 300
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,4 +78,6 @@ struct SettingsData: Codable {
|
|||||||
let pageSize: Int
|
let pageSize: Int
|
||||||
let passcode: String
|
let passcode: String
|
||||||
let key: String
|
let key: String
|
||||||
|
let notificationsEnabled: Bool
|
||||||
|
let notificationInterval: Int
|
||||||
}
|
}
|
||||||
@ -107,6 +107,13 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section("Уведомления") {
|
||||||
|
NavigationLink("Настройки уведомлений") {
|
||||||
|
NotificationSettingsView()
|
||||||
|
.environmentObject(apiClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section("Управление кэшем") {
|
Section("Управление кэшем") {
|
||||||
Button("Очистить кэш досок") {
|
Button("Очистить кэш досок") {
|
||||||
Cache.shared.delete("boards")
|
Cache.shared.delete("boards")
|
||||||
@ -176,6 +183,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $showingDebugMenu) {
|
.sheet(isPresented: $showingDebugMenu) {
|
||||||
DebugMenuView()
|
DebugMenuView()
|
||||||
|
.environmentObject(NotificationManager.shared)
|
||||||
}
|
}
|
||||||
.alert("Информация о НЕОЖИДАНЫХ проблемах", isPresented: $showingInfo) {
|
.alert("Информация о НЕОЖИДАНЫХ проблемах", isPresented: $showingInfo) {
|
||||||
Button("Закрыть") { }
|
Button("Закрыть") { }
|
||||||
@ -275,6 +283,7 @@ struct AboutView: View {
|
|||||||
|
|
||||||
struct DebugMenuView: View {
|
struct DebugMenuView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@EnvironmentObject var notificationManager: NotificationManager
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
@ -290,6 +299,12 @@ struct DebugMenuView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
|
|
||||||
|
Button("Тест уведомления") {
|
||||||
|
notificationManager.scheduleTestNotification()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.foregroundColor(.blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@ -4,6 +4,7 @@ struct ThreadsView: View {
|
|||||||
let board: Board
|
let board: Board
|
||||||
@EnvironmentObject var settings: Settings
|
@EnvironmentObject var settings: Settings
|
||||||
@EnvironmentObject var apiClient: APIClient
|
@EnvironmentObject var apiClient: APIClient
|
||||||
|
@EnvironmentObject var notificationManager: NotificationManager
|
||||||
@State private var threads: [Thread] = []
|
@State private var threads: [Thread] = []
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@State private var errorMessage: String?
|
@State private var errorMessage: String?
|
||||||
|
|||||||
39
README.md
39
README.md
@ -30,6 +30,13 @@
|
|||||||
- Кэширование данных
|
- Кэширование данных
|
||||||
- Оптимизация потребления батареи
|
- Оптимизация потребления батареи
|
||||||
- Поддержка iOS 15.0+
|
- Поддержка iOS 15.0+
|
||||||
|
- **Push-уведомления о новых тредах:**
|
||||||
|
- Подписка на доски через тумблеры в настройках
|
||||||
|
- Настраиваемый интервал проверки (5 мин - 1 час)
|
||||||
|
- Фоновое обновление
|
||||||
|
- Задержка уведомлений 10 секунд
|
||||||
|
- Формат: "Новый тред: [название] в /boardname/"
|
||||||
|
- Тестовые уведомления в debug меню
|
||||||
|
|
||||||
## Аутентификация и постинг
|
## Аутентификация и постинг
|
||||||
|
|
||||||
@ -54,6 +61,33 @@
|
|||||||
3. Введите текст комментария
|
3. Введите текст комментария
|
||||||
4. Нажмите "Добавить"
|
4. Нажмите "Добавить"
|
||||||
|
|
||||||
|
## Уведомления о новых тредах
|
||||||
|
|
||||||
|
### Настройка уведомлений
|
||||||
|
|
||||||
|
1. Откройте настройки приложения
|
||||||
|
2. Перейдите в "Настройки уведомлений"
|
||||||
|
3. Включите уведомления
|
||||||
|
4. Разрешите уведомления в системных настройках iOS
|
||||||
|
5. Настройте интервал проверки (5 мин - 1 час)
|
||||||
|
|
||||||
|
### Подписка на доски
|
||||||
|
|
||||||
|
1. Откройте настройки приложения
|
||||||
|
2. Перейдите в "Настройки уведомлений"
|
||||||
|
3. Включите уведомления
|
||||||
|
4. В разделе "Подписки на доски" включите тумблеры для нужных досок
|
||||||
|
5. Для отписки отключите соответствующий тумблер
|
||||||
|
|
||||||
|
### Как это работает
|
||||||
|
|
||||||
|
- Приложение периодически проверяет новые треды в фоне
|
||||||
|
- При обнаружении нового треда отправляется push-уведомление через 10 секунд
|
||||||
|
- Формат уведомления: "Новый тред: [название] в /boardname/"
|
||||||
|
- Подписки сохраняются между запусками приложения
|
||||||
|
- Управление подписками через тумблеры в настройках уведомлений
|
||||||
|
- Тестовые уведомления доступны в debug меню (5 нажатий на информацию об устройстве)
|
||||||
|
|
||||||
## Сборка
|
## Сборка
|
||||||
|
|
||||||
### Требования
|
### Требования
|
||||||
@ -92,6 +126,9 @@ open MobileMkch.xcodeproj
|
|||||||
- `CreateThreadView.swift` - создание тредов
|
- `CreateThreadView.swift` - создание тредов
|
||||||
- `AddCommentView.swift` - добавление комментариев
|
- `AddCommentView.swift` - добавление комментариев
|
||||||
- `SettingsView.swift` - экран настроек
|
- `SettingsView.swift` - экран настроек
|
||||||
|
- `NotificationManager.swift` - управление уведомлениями и тестовые уведомления
|
||||||
|
- `BackgroundTaskManager.swift` - фоновые задачи
|
||||||
|
- `NotificationSettingsView.swift` - настройки уведомлений с тумблерами подписок
|
||||||
|
|
||||||
## Технологии
|
## Технологии
|
||||||
|
|
||||||
@ -105,4 +142,4 @@ open MobileMkch.xcodeproj
|
|||||||
- iOS 15.0+
|
- iOS 15.0+
|
||||||
- iPhone и iPad (айпад СЛЕГКА поломан, проверил, мб чет с этим сделаю, лень)
|
- iPhone и iPad (айпад СЛЕГКА поломан, проверил, мб чет с этим сделаю, лень)
|
||||||
- Поддержка темной/светлой темы
|
- Поддержка темной/светлой темы
|
||||||
- Адаптивный интерфейс
|
- Адаптивный интерфейс
|
||||||
Loading…
x
Reference in New Issue
Block a user