567 lines
23 KiB
Swift
567 lines
23 KiB
Swift
import SwiftUI
|
||
import Combine
|
||
import Darwin
|
||
|
||
struct SettingsView: View {
|
||
@EnvironmentObject var settings: Settings
|
||
@EnvironmentObject var apiClient: APIClient
|
||
@EnvironmentObject var networkMonitor: NetworkMonitor
|
||
|
||
@State private var showingAbout = false
|
||
@State private var showingInfo = false
|
||
@State private var testKeyResult: String?
|
||
@State private var testPasscodeResult: String?
|
||
@State private var isTestingKey = false
|
||
@State private var isTestingPasscode = false
|
||
@State private var debugTapCount = 0
|
||
@State private var showingDebugMenu = false
|
||
@State private var showingUnstableWarning = false
|
||
|
||
var body: some View {
|
||
NavigationView {
|
||
Form {
|
||
Section("Внешний вид") {
|
||
HStack {
|
||
Image(systemName: "moon.fill")
|
||
.foregroundColor(.purple)
|
||
.frame(width: 24)
|
||
Text("Тема")
|
||
Spacer()
|
||
Picker("", selection: $settings.theme) {
|
||
Text("Темная").tag("dark")
|
||
Text("Светлая").tag("light")
|
||
}
|
||
.pickerStyle(SegmentedPickerStyle())
|
||
.frame(width: 120)
|
||
}
|
||
.onReceive(Just(settings.theme)) { _ in
|
||
settings.saveSettings()
|
||
}
|
||
|
||
HStack {
|
||
Image(systemName: "arrow.clockwise")
|
||
.foregroundColor(.green)
|
||
.frame(width: 24)
|
||
Toggle("Авторефреш", isOn: $settings.autoRefresh)
|
||
}
|
||
.onReceive(Just(settings.autoRefresh)) { _ in
|
||
settings.saveSettings()
|
||
}
|
||
|
||
HStack {
|
||
Image(systemName: "paperclip")
|
||
.foregroundColor(.blue)
|
||
.frame(width: 24)
|
||
Toggle("Показывать файлы", isOn: $settings.showFiles)
|
||
}
|
||
.onReceive(Just(settings.showFiles)) { _ in
|
||
settings.saveSettings()
|
||
}
|
||
|
||
HStack {
|
||
Image(systemName: "rectangle.compress.vertical")
|
||
.foregroundColor(.orange)
|
||
.frame(width: 24)
|
||
Toggle("Компактный режим", isOn: $settings.compactMode)
|
||
}
|
||
.onReceive(Just(settings.compactMode)) { _ in
|
||
settings.saveSettings()
|
||
}
|
||
|
||
HStack {
|
||
Image(systemName: "list.bullet")
|
||
.foregroundColor(.indigo)
|
||
.frame(width: 24)
|
||
Text("Размер страницы")
|
||
Spacer()
|
||
Picker("", selection: $settings.pageSize) {
|
||
Text("5").tag(5)
|
||
Text("10").tag(10)
|
||
Text("15").tag(15)
|
||
Text("20").tag(20)
|
||
}
|
||
.pickerStyle(MenuPickerStyle())
|
||
}
|
||
.onReceive(Just(settings.pageSize)) { _ in
|
||
settings.saveSettings()
|
||
}
|
||
|
||
HStack {
|
||
Image(systemName: "rectangle.split.2x1")
|
||
.foregroundColor(.teal)
|
||
.frame(width: 24)
|
||
Toggle("Разстраничивание", isOn: $settings.enablePagination)
|
||
}
|
||
.onReceive(Just(settings.enablePagination)) { _ in
|
||
settings.saveSettings()
|
||
}
|
||
|
||
HStack {
|
||
Image(systemName: "exclamationmark.triangle.fill")
|
||
.foregroundColor(.red)
|
||
.frame(width: 24)
|
||
Toggle("Нестабильные функции", isOn: $settings.enableUnstableFeatures)
|
||
}
|
||
.onReceive(Just(settings.enableUnstableFeatures)) { newValue in
|
||
if newValue && !UserDefaults.standard.bool(forKey: "hasShownUnstableWarning") {
|
||
showingUnstableWarning = true
|
||
UserDefaults.standard.set(true, forKey: "hasShownUnstableWarning")
|
||
}
|
||
settings.saveSettings()
|
||
}
|
||
}
|
||
|
||
Section("Оффлайн режим") {
|
||
HStack {
|
||
Image(systemName: "wifi.slash")
|
||
.foregroundColor(.orange)
|
||
.frame(width: 24)
|
||
Toggle("Принудительно оффлайн", isOn: $settings.offlineMode)
|
||
}
|
||
.onChange(of: settings.offlineMode) { newValue in
|
||
if networkMonitor.forceOffline != newValue {
|
||
networkMonitor.forceOffline = newValue
|
||
settings.saveSettings()
|
||
}
|
||
}
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text(networkMonitor.offlineEffective ? "Сейчас оффлайн: показываем кэш" : "Онлайн: будут загружаться свежие данные")
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
}
|
||
|
||
Section("Аутентификация") {
|
||
HStack {
|
||
Image(systemName: "lock.shield")
|
||
.foregroundColor(.orange)
|
||
.frame(width: 24)
|
||
SecureField("Passcode для постинга", text: $settings.passcode)
|
||
}
|
||
.onReceive(Just(settings.passcode)) { _ in
|
||
settings.saveSettings()
|
||
}
|
||
|
||
HStack {
|
||
Image(systemName: "key")
|
||
.foregroundColor(.blue)
|
||
.frame(width: 24)
|
||
SecureField("Ключ аутентификации", text: $settings.key)
|
||
}
|
||
.onReceive(Just(settings.key)) { _ in
|
||
settings.saveSettings()
|
||
}
|
||
|
||
HStack {
|
||
Button("Тест ключа") {
|
||
testKey()
|
||
}
|
||
.disabled(settings.key.isEmpty || isTestingKey)
|
||
|
||
if isTestingKey {
|
||
ProgressView()
|
||
.scaleEffect(0.8)
|
||
}
|
||
|
||
if let result = testKeyResult {
|
||
Text(result)
|
||
.font(.caption)
|
||
.foregroundColor(result.contains("успешно") ? .green : .red)
|
||
}
|
||
}
|
||
|
||
HStack {
|
||
Button("Тест passcode") {
|
||
testPasscode()
|
||
}
|
||
.disabled(settings.passcode.isEmpty || isTestingPasscode)
|
||
|
||
if isTestingPasscode {
|
||
ProgressView()
|
||
.scaleEffect(0.8)
|
||
}
|
||
|
||
if let result = testPasscodeResult {
|
||
Text(result)
|
||
.font(.caption)
|
||
.foregroundColor(result.contains("успешно") ? .green : .red)
|
||
}
|
||
}
|
||
}
|
||
|
||
Section("Уведомления") {
|
||
NavigationLink("Настройки уведомлений") {
|
||
NotificationSettingsView()
|
||
.environmentObject(apiClient)
|
||
}
|
||
.overlay(
|
||
HStack {
|
||
Spacer()
|
||
Image(systemName: "sparkles")
|
||
.font(.caption2)
|
||
.foregroundColor(.orange)
|
||
}
|
||
.padding(.trailing, 8)
|
||
)
|
||
}
|
||
|
||
Section("Управление кэшем") {
|
||
Button(action: {
|
||
Cache.shared.delete("boards")
|
||
}) {
|
||
HStack {
|
||
Image(systemName: "list.bullet")
|
||
.foregroundColor(.blue)
|
||
.frame(width: 24)
|
||
Text("Очистить кэш досок")
|
||
Spacer()
|
||
}
|
||
}
|
||
|
||
Button(action: {
|
||
apiClient.getBoards { result in
|
||
if case .success(let boards) = result {
|
||
for board in boards {
|
||
Cache.shared.delete("threads_\(board.code)")
|
||
}
|
||
}
|
||
}
|
||
}) {
|
||
HStack {
|
||
Image(systemName: "doc.text")
|
||
.foregroundColor(.green)
|
||
.frame(width: 24)
|
||
Text("Очистить кэш тредов")
|
||
Spacer()
|
||
}
|
||
}
|
||
|
||
Button(action: {
|
||
settings.clearImageCache()
|
||
}) {
|
||
HStack {
|
||
Image(systemName: "photo")
|
||
.foregroundColor(.purple)
|
||
.frame(width: 24)
|
||
Text("Очистить кэш изображений")
|
||
Spacer()
|
||
}
|
||
}
|
||
|
||
Button(action: {
|
||
Cache.shared.clear()
|
||
settings.clearImageCache()
|
||
}) {
|
||
HStack {
|
||
Image(systemName: "trash")
|
||
.foregroundColor(.red)
|
||
.frame(width: 24)
|
||
Text("Очистить весь кэш")
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
|
||
Section {
|
||
Button(action: {
|
||
settings.resetSettings()
|
||
}) {
|
||
HStack {
|
||
Image(systemName: "arrow.counterclockwise")
|
||
.foregroundColor(.red)
|
||
.frame(width: 24)
|
||
Text("Сбросить настройки")
|
||
.foregroundColor(.red)
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
|
||
Section {
|
||
Button(action: {
|
||
showingAbout = true
|
||
}) {
|
||
HStack {
|
||
Image(systemName: "info.circle")
|
||
.foregroundColor(.blue)
|
||
.frame(width: 24)
|
||
Text("Об аппке")
|
||
Spacer()
|
||
}
|
||
}
|
||
|
||
Button(action: {
|
||
showingInfo = true
|
||
}) {
|
||
HStack {
|
||
Image(systemName: "exclamationmark.triangle")
|
||
.foregroundColor(.orange)
|
||
.frame(width: 24)
|
||
Text("Я думаю тебя направили сюда")
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
|
||
Section("Информация об устройстве") {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text("Устройство: \(getDeviceModel())")
|
||
Text("Система: \(UIDevice.current.systemName) \(UIDevice.current.systemVersion)")
|
||
Text("Тип: \(UIDevice.current.name.isEmpty ? "Не удалось определить, увы" : UIDevice.current.name)")
|
||
Text("Идентификатор: \(UIDevice.current.identifierForVendor?.uuidString ?? "Неизвестно")")
|
||
}
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
.onTapGesture {
|
||
debugTapCount += 1
|
||
if debugTapCount >= 5 {
|
||
showingDebugMenu = true
|
||
debugTapCount = 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("Настройки")
|
||
.navigationBarTitleDisplayMode(.large)
|
||
.sheet(isPresented: $showingAbout) {
|
||
AboutView()
|
||
}
|
||
.sheet(isPresented: $showingDebugMenu) {
|
||
DebugMenuView()
|
||
.environmentObject(NotificationManager.shared)
|
||
}
|
||
.sheet(isPresented: $showingUnstableWarning) {
|
||
UnstableFeaturesWarningView(isPresented: $showingUnstableWarning)
|
||
}
|
||
.alert("Информация о НЕОЖИДАНЫХ проблемах", isPresented: $showingInfo) {
|
||
Button("Закрыть") { }
|
||
} message: {
|
||
Text("Если тебя направили сюда, то значит ты попал на НЕИЗВЕДАННЫЕ ТЕРРИТОРИИ\n\nДА ДА, не ослышались, это не ошибка, это особенность\n\nУвы, разработчик имиджборда вставил палки в колеса\n\nИ без доната ему, например, постинг работать не будет\n\nУвы, постинг не работает без доната, а разработчик боится что на его сайте будут спам\n\nВкратце - на сайте работает капча, а наличие пасскода ее для вас отключает\n\nувы, конфет много, но на всех не хватит")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
extension SettingsView {
|
||
private func testKey() {
|
||
guard !settings.key.isEmpty else { return }
|
||
|
||
isTestingKey = true
|
||
testKeyResult = nil
|
||
|
||
apiClient.authenticate(authKey: settings.key) { error in
|
||
DispatchQueue.main.async {
|
||
self.isTestingKey = false
|
||
|
||
if let error = error {
|
||
self.testKeyResult = "Ошибка: \(error.localizedDescription)"
|
||
} else {
|
||
self.testKeyResult = "Аутентификация успешна"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func testPasscode() {
|
||
guard !settings.passcode.isEmpty else { return }
|
||
|
||
isTestingPasscode = true
|
||
testPasscodeResult = nil
|
||
|
||
apiClient.loginWithPasscode(passcode: settings.passcode) { error in
|
||
DispatchQueue.main.async {
|
||
self.isTestingPasscode = false
|
||
|
||
if let error = error {
|
||
self.testPasscodeResult = "Ошибка: \(error.localizedDescription)"
|
||
} else {
|
||
self.testPasscodeResult = "Вход успешен"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func getDeviceModel() -> String {
|
||
var systemInfo = utsname()
|
||
uname(&systemInfo)
|
||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||
let identifier = machineMirror.children.reduce("") { identifier, element in
|
||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||
}
|
||
|
||
return identifier
|
||
}
|
||
}
|
||
|
||
struct AboutView: View {
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
var body: some View {
|
||
NavigationView {
|
||
VStack(spacing: 20) {
|
||
Text("MobileMkch")
|
||
.font(.largeTitle)
|
||
.fontWeight(.bold)
|
||
|
||
Text("Мобильный клиент для мкача")
|
||
.font(.title3)
|
||
.foregroundColor(.secondary)
|
||
|
||
Divider()
|
||
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text("Версия: 2.1.0-ios-alpha (Always in alpha lol)")
|
||
Text("Автор: w^x (лейн, платон, а похуй как угодно)")
|
||
Text("Разработано с <3 на Свифт")
|
||
}
|
||
.font(.body)
|
||
|
||
Spacer()
|
||
|
||
Button("Закрыть") {
|
||
dismiss()
|
||
}
|
||
.buttonStyle(.bordered)
|
||
}
|
||
.padding()
|
||
.navigationTitle("Об аппке")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
}
|
||
}
|
||
}
|
||
|
||
struct DebugMenuView: View {
|
||
@Environment(\.dismiss) private var dismiss
|
||
@EnvironmentObject var notificationManager: NotificationManager
|
||
|
||
var body: some View {
|
||
NavigationView {
|
||
VStack(spacing: 20) {
|
||
Text("Debug Menu")
|
||
.font(.largeTitle)
|
||
.fontWeight(.bold)
|
||
|
||
VStack(spacing: 15) {
|
||
Button("Тест краша") {
|
||
CrashHandler.shared.triggerTestCrash()
|
||
dismiss()
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.foregroundColor(.red)
|
||
|
||
Button("Тест уведомления") {
|
||
notificationManager.scheduleTestNotification()
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.foregroundColor(.blue)
|
||
}
|
||
|
||
Spacer()
|
||
|
||
Button("Закрыть") {
|
||
dismiss()
|
||
}
|
||
.buttonStyle(.bordered)
|
||
}
|
||
.padding()
|
||
.navigationTitle("Debug")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
}
|
||
}
|
||
}
|
||
|
||
struct UnstableFeaturesWarningView: View {
|
||
@Binding var isPresented: Bool
|
||
@State private var timeRemaining = 10
|
||
@State private var canConfirm = false
|
||
|
||
var body: some View {
|
||
NavigationView {
|
||
VStack(spacing: 20) {
|
||
Image(systemName: "exclamationmark.triangle.fill")
|
||
.font(.system(size: 60))
|
||
.foregroundColor(.red)
|
||
|
||
Text("ВНИМАНИЕ!")
|
||
.font(.largeTitle)
|
||
.fontWeight(.bold)
|
||
.foregroundColor(.red)
|
||
|
||
VStack(spacing: 12) {
|
||
Text("Вы собираетесь включить нестабильные функции")
|
||
.font(.headline)
|
||
.multilineTextAlignment(.center)
|
||
|
||
Text("Эти функции находятся в разработке и могут:")
|
||
.font(.body)
|
||
.foregroundColor(.secondary)
|
||
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text("Работать нестабильно или не работать вовсе")
|
||
Text("Вызывать краши приложения")
|
||
Text("Потреблять больше ресурсов")
|
||
Text("Иметь неожиданное поведение")
|
||
}
|
||
.font(.body)
|
||
.foregroundColor(.secondary)
|
||
|
||
Text("НИКАКИЕ ЖАЛОБЫ на нестабильный функционал НЕ ПРИНИМАЮТСЯ!")
|
||
.font(.headline)
|
||
.fontWeight(.bold)
|
||
.foregroundColor(.red)
|
||
.multilineTextAlignment(.center)
|
||
.padding(.top)
|
||
}
|
||
|
||
Spacer()
|
||
|
||
VStack(spacing: 16) {
|
||
if !canConfirm {
|
||
Text("Подтверждение будет доступно через: \(timeRemaining)")
|
||
.font(.headline)
|
||
.foregroundColor(.orange)
|
||
}
|
||
|
||
Button(action: {
|
||
isPresented = false
|
||
}) {
|
||
Text(canConfirm ? "Я уверен!" : "Отмена")
|
||
.font(.headline)
|
||
.fontWeight(.bold)
|
||
.foregroundColor(.white)
|
||
.frame(maxWidth: .infinity)
|
||
.padding()
|
||
.background(canConfirm ? Color.red : Color.gray)
|
||
.cornerRadius(10)
|
||
}
|
||
.disabled(!canConfirm)
|
||
}
|
||
}
|
||
.padding()
|
||
.navigationTitle("Предупреждение")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.onAppear {
|
||
startTimer()
|
||
}
|
||
}
|
||
}
|
||
|
||
private func startTimer() {
|
||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
|
||
if timeRemaining > 0 {
|
||
timeRemaining -= 1
|
||
} else {
|
||
canConfirm = true
|
||
timer.invalidate()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
SettingsView()
|
||
.environmentObject(Settings())
|
||
.environmentObject(APIClient())
|
||
}
|