iOS update: SettingsView.swift

This commit is contained in:
efir369999 2026-05-05 17:16:29 +03:00
parent 4f5f24f132
commit 4d28739503

View File

@ -1,61 +1,58 @@
import SwiftUI import SwiftUI
import Combine import Combine
@MainActor
final class SettingsStore: ObservableObject {
static let shared = SettingsStore()
@AppStorage("settings.notifications") var notifications: Bool = true
@AppStorage("settings.faceID") var faceID: Bool = true
@AppStorage("settings.silentMode") var silentMode: Bool = false
}
struct SettingsView: View { struct SettingsView: View {
@StateObject private var store = SettingsStore.shared @EnvironmentObject private var identity: IdentityManager
@State private var showAbout = false
@State private var showAccountID = false @State private var showAccountID = false
@State private var showResetAlert = false @State private var showAbout = false
@State private var showExport = false
@State private var showWipe = false
@State private var editingName = false
@State private var nameInput = ""
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ScrollView { ScrollView {
VStack(spacing: ClaudeTheme.Spacing.xl) { VStack(spacing: ClaudeTheme.Spacing.xl) {
ProfileHeader() if let id = identity.identity {
ProfileHeader(identity: id, onEditName: {
nameInput = id.displayName
editingName = true
})
.padding(.top, ClaudeTheme.Spacing.xl) .padding(.top, ClaudeTheme.Spacing.xl)
.padding(.bottom, ClaudeTheme.Spacing.sm)
SettingsSection(title: "Идентичность") { SettingsSection(title: "Идентичность") {
SettingsRow(icon: "key.fill", title: "Mnemonic фраза", subtitle: "12 слов · BIP-39") {} SettingsRow(icon: "person.text.rectangle.fill",
SettingsRow(icon: "person.text.rectangle.fill", title: "Account ID", subtitle: "0x7a3f…b921") { title: "Account ID",
subtitle: id.accountID) {
showAccountID = true showAccountID = true
} }
SettingsRow(icon: "qrcode", title: "Поделиться контактом") {} SettingsRow(icon: "key.fill",
title: "Экспорт ключей",
subtitle: "только на новое устройство") {
showExport = true
}
} }
SettingsSection(title: "Безопасность") { SettingsSection(title: "Сеть") {
SettingsToggle(icon: "faceid", title: "Face ID", isOn: $store.faceID) SettingsInfo(icon: "antenna.radiowaves.left.and.right",
SettingsRow(icon: "lock.shield.fill", title: "PIN-код", subtitle: "Включён") {} title: "Сервер",
SettingsRow(icon: "shield.lefthalf.filled", title: "Постквантовая криптография", subtitle: "ML-DSA-65") {} value: "mess.montana.quest")
} SettingsInfo(icon: "shield.lefthalf.filled",
title: "Шифрование",
SettingsSection(title: "Уведомления") { value: "X25519 + ChaCha20-Poly1305")
SettingsToggle(icon: "bell.fill", title: "Push-уведомления", isOn: $store.notifications)
SettingsToggle(icon: "moon.fill", title: "Не беспокоить", isOn: $store.silentMode)
}
SettingsSection(title: "Сеть Montana") {
SettingsRow(icon: "network", title: "Узлы", subtitle: "мос · фра · зел") {}
SettingsRow(icon: "clock.arrow.2.circlepath", title: "TimeChain", subtitle: "Окно 2891844") {}
SettingsRow(icon: "antenna.radiowaves.left.and.right", title: "Соединение", subtitle: "Helsinki Reality") {}
} }
SettingsSection(title: "О приложении") { SettingsSection(title: "О приложении") {
SettingsRow(icon: "info.circle.fill", title: "Версия", subtitle: "0.1.0 (1)") {} SettingsRow(icon: "doc.text.fill",
SettingsRow(icon: "doc.text.fill", title: "Манифест Монтаны") { showAbout = true } title: "Манифест Монтаны") { showAbout = true }
SettingsRow(icon: "heart.fill", title: "Автор", subtitle: "Алик Монтана") {} SettingsInfo(icon: "info.circle.fill",
title: "Версия",
value: "1.0.0 (1)")
} }
Button { showResetAlert = true } label: { Button(role: .destructive) { showWipe = true } label: {
Text("Сбросить идентичность") Text("Стереть идентичность")
.font(ClaudeTheme.Typography.headline) .font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.danger) .foregroundStyle(ClaudeTheme.Palette.danger)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -74,35 +71,57 @@ struct SettingsView: View {
Footer().padding(.bottom, ClaudeTheme.Spacing.xxl) Footer().padding(.bottom, ClaudeTheme.Spacing.xxl)
} }
} }
}
.claudeBackground() .claudeBackground()
.navigationTitle("Настройки") .navigationTitle("Настройки")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.alert("Сбросить идентичность?", isPresented: $showResetAlert) { .alert("Стереть идентичность?", isPresented: $showWipe) {
Button("Отмена", role: .cancel) {} Button("Отмена", role: .cancel) {}
Button("Сбросить", role: .destructive) {} Button("Стереть", role: .destructive) { identity.wipe() }
} message: { } message: {
Text("Все ключи и сообщения будут стёрты с устройства.") Text("Все ключи и переписка будут удалены с устройства. Если у тебя нет резервной копии ключей — восстановить аккаунт будет невозможно.")
}
.alert("Изменить имя", isPresented: $editingName) {
TextField("Имя", text: $nameInput)
Button("Сохранить") {
let trimmed = nameInput.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty { identity.setDisplayName(trimmed) }
}
Button("Отмена", role: .cancel) {}
}
.sheet(isPresented: $showAbout) { AboutView() }
.sheet(isPresented: $showAccountID) {
if let id = identity.identity { AccountIDView(identity: id) }
}
.sheet(isPresented: $showExport) {
if let id = identity.identity { ExportKeysView(identity: id) }
} }
.sheet(isPresented: $showAbout) { AboutMontanaView() }
.sheet(isPresented: $showAccountID) { AccountIDView() }
} }
} }
} }
private struct ProfileHeader: View { private struct ProfileHeader: View {
let identity: Identity
let onEditName: () -> Void
var body: some View { var body: some View {
VStack(spacing: ClaudeTheme.Spacing.md) { VStack(spacing: ClaudeTheme.Spacing.md) {
AvatarView(name: "Алик Монтана", size: 96) AvatarView(name: identity.displayName, size: 96)
.shadow(color: ClaudeTheme.Palette.gold.opacity(0.35), radius: 18, y: 4) .shadow(color: ClaudeTheme.Palette.gold.opacity(0.35), radius: 18, y: 4)
VStack(spacing: 4) { Button(action: onEditName) {
Text("Алик Монтана") HStack(spacing: 6) {
Text(identity.displayName)
.font(.system(size: 24, weight: .semibold, design: .serif)) .font(.system(size: 24, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.textPrimary) .foregroundStyle(ClaudeTheme.Palette.textPrimary)
Text("@alik · 0x7a3f…b921") Image(systemName: "pencil")
.font(ClaudeTheme.Typography.caption) .font(.system(size: 12))
.foregroundStyle(ClaudeTheme.Palette.textTertiary) .foregroundStyle(ClaudeTheme.Palette.textTertiary)
} }
} }
Text(identity.accountID)
.font(ClaudeTheme.Typography.mono)
.foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.7))
}
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
@ -113,12 +132,12 @@ private struct Footer: View {
Text("Ӂ") Text("Ӂ")
.font(.system(size: 36, weight: .semibold, design: .serif)) .font(.system(size: 36, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.goldGradient) .foregroundStyle(ClaudeTheme.Palette.goldGradient)
Text("MONTANA · TIMECHAIN") Text("MONTANA")
.font(.system(size: 10, weight: .medium)) .font(.system(size: 10, weight: .medium))
.tracking(3) .tracking(3)
.foregroundStyle(ClaudeTheme.Palette.textTertiary) .foregroundStyle(ClaudeTheme.Palette.textTertiary)
Text("XXIIVIIIMMXXXI") Text("XXIIVIIIMMXXXI")
.font(.system(size: 9, weight: .regular, design: .serif)) .font(.system(size: 9, design: .serif))
.tracking(2) .tracking(2)
.foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.45)) .foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.45))
} }
@ -159,7 +178,10 @@ private struct SettingsRow: View {
let action: () -> Void let action: () -> Void
var body: some View { var body: some View {
Button(action: action) { Button(action: action) { rowBody(showChevron: true) }
}
private func rowBody(showChevron: Bool) -> some View {
HStack(spacing: ClaudeTheme.Spacing.md) { HStack(spacing: ClaudeTheme.Spacing.md) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 14)) .font(.system(size: 14))
@ -177,30 +199,29 @@ private struct SettingsRow: View {
Text(subtitle) Text(subtitle)
.font(ClaudeTheme.Typography.caption) .font(ClaudeTheme.Typography.caption)
.foregroundStyle(ClaudeTheme.Palette.textTertiary) .foregroundStyle(ClaudeTheme.Palette.textTertiary)
.lineLimit(1)
} }
} }
Spacer() Spacer()
if showChevron {
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold)) .font(.system(size: 12, weight: .semibold))
.foregroundStyle(ClaudeTheme.Palette.textTertiary) .foregroundStyle(ClaudeTheme.Palette.textTertiary)
} }
}
.padding(.horizontal, ClaudeTheme.Spacing.md) .padding(.horizontal, ClaudeTheme.Spacing.md)
.padding(.vertical, ClaudeTheme.Spacing.md) .padding(.vertical, ClaudeTheme.Spacing.md)
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
Rectangle() Rectangle().fill(ClaudeTheme.Palette.divider).frame(height: 0.5).padding(.leading, 56)
.fill(ClaudeTheme.Palette.divider)
.frame(height: 0.5)
.padding(.leading, 56)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
}
} }
private struct SettingsToggle: View { private struct SettingsInfo: View {
let icon: String let icon: String
let title: String let title: String
@Binding var isOn: Bool let value: String
var body: some View { var body: some View {
HStack(spacing: ClaudeTheme.Spacing.md) { HStack(spacing: ClaudeTheme.Spacing.md) {
@ -216,98 +237,30 @@ private struct SettingsToggle: View {
.font(ClaudeTheme.Typography.body) .font(ClaudeTheme.Typography.body)
.foregroundStyle(ClaudeTheme.Palette.textPrimary) .foregroundStyle(ClaudeTheme.Palette.textPrimary)
Spacer() Spacer()
Toggle("", isOn: $isOn) Text(value)
.labelsHidden() .font(ClaudeTheme.Typography.caption)
.tint(ClaudeTheme.Palette.gold) .foregroundStyle(ClaudeTheme.Palette.textTertiary)
} }
.padding(.horizontal, ClaudeTheme.Spacing.md) .padding(.horizontal, ClaudeTheme.Spacing.md)
.padding(.vertical, ClaudeTheme.Spacing.md) .padding(.vertical, ClaudeTheme.Spacing.md)
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
Rectangle() Rectangle().fill(ClaudeTheme.Palette.divider).frame(height: 0.5).padding(.leading, 56)
.fill(ClaudeTheme.Palette.divider)
.frame(height: 0.5)
.padding(.leading, 56)
}
}
}
private struct AboutMontanaView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) {
Text("Ӂ")
.font(.system(size: 80, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.goldGradient)
.frame(maxWidth: .infinity)
.padding(.top, ClaudeTheme.Spacing.xl)
VStack(spacing: 4) {
Text("MONTANA")
.font(.system(size: 28, weight: .bold, design: .serif))
.tracking(8)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
Text("TIMECHAIN PROTOCOL")
.font(.system(size: 11, weight: .medium))
.tracking(4)
.foregroundStyle(ClaudeTheme.Palette.gold)
}
.frame(maxWidth: .infinity)
VStack(alignment: .leading, spacing: 12) {
Text("Цифровая собственность.")
Text("Постквантовая криптография.")
Text("Твой ключ — твоя идентичность.")
Text("Никто не читает твои сообщения. Никто не знает, кто ты. Это нормально.")
.padding(.top, 8)
.foregroundStyle(ClaudeTheme.Palette.textSecondary)
}
.font(ClaudeTheme.Typography.serif)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
.padding(.horizontal, ClaudeTheme.Spacing.xl)
.padding(.top, ClaudeTheme.Spacing.lg)
Spacer()
VStack(spacing: 4) {
Text("XXIIVIIIMMXXXI")
.font(.system(size: 11, design: .serif))
.tracking(2)
.foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.6))
Text("Genesis · 9.01.2026")
.font(.system(size: 10))
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
}
.frame(maxWidth: .infinity)
.padding(.bottom, ClaudeTheme.Spacing.xl)
}
}
.claudeBackground()
.navigationTitle("Манифест")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Готово") { dismiss() }
.foregroundStyle(ClaudeTheme.Palette.gold)
}
}
} }
} }
} }
private struct AccountIDView: View { private struct AccountIDView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
private let accountID = "0x7a3f9b1c8d4e2f0a5b6c9d8e7f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f921" let identity: Identity
var body: some View { var body: some View {
NavigationStack { NavigationStack {
VStack(spacing: ClaudeTheme.Spacing.xl) { VStack(spacing: ClaudeTheme.Spacing.xl) {
AvatarView(name: "Алик Монтана", size: 120) AvatarView(name: identity.displayName, size: 120)
.padding(.top, ClaudeTheme.Spacing.xxl) .padding(.top, ClaudeTheme.Spacing.xxl)
.shadow(color: ClaudeTheme.Palette.gold.opacity(0.4), radius: 22, y: 6) .shadow(color: ClaudeTheme.Palette.gold.opacity(0.4), radius: 22, y: 6)
Text("Алик Монтана") Text(identity.displayName)
.font(ClaudeTheme.Typography.title) .font(ClaudeTheme.Typography.title)
.foregroundStyle(ClaudeTheme.Palette.textPrimary) .foregroundStyle(ClaudeTheme.Palette.textPrimary)
@ -316,10 +269,9 @@ private struct AccountIDView: View {
.font(.system(size: 11, weight: .medium)) .font(.system(size: 11, weight: .medium))
.tracking(2) .tracking(2)
.foregroundStyle(ClaudeTheme.Palette.textTertiary) .foregroundStyle(ClaudeTheme.Palette.textTertiary)
Text(accountID) Text(identity.accountID)
.font(ClaudeTheme.Typography.mono) .font(.system(size: 18, design: .monospaced))
.foregroundStyle(ClaudeTheme.Palette.gold) .foregroundStyle(ClaudeTheme.Palette.gold)
.multilineTextAlignment(.center)
.padding(ClaudeTheme.Spacing.md) .padding(ClaudeTheme.Spacing.md)
.background( .background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous) RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
@ -332,8 +284,14 @@ private struct AccountIDView: View {
} }
.padding(.horizontal, ClaudeTheme.Spacing.lg) .padding(.horizontal, ClaudeTheme.Spacing.lg)
Text("Поделись этим Account ID с тем, кто хочет тебе написать.")
.font(ClaudeTheme.Typography.caption)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
.multilineTextAlignment(.center)
.padding(.horizontal, ClaudeTheme.Spacing.xl)
Button { Button {
UIPasteboard.general.string = accountID UIPasteboard.general.string = identity.accountID
} label: { } label: {
HStack { HStack {
Image(systemName: "doc.on.doc") Image(systemName: "doc.on.doc")
@ -360,3 +318,145 @@ private struct AccountIDView: View {
} }
} }
} }
private struct ExportKeysView: View {
@Environment(\.dismiss) private var dismiss
let identity: Identity
@State private var revealed = false
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) {
HStack(spacing: 12) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(ClaudeTheme.Palette.danger)
Text("Кто угодно с этими ключами может выдать себя за тебя. Не делись ими ни с кем кроме своих устройств.")
.font(ClaudeTheme.Typography.callout)
.foregroundStyle(ClaudeTheme.Palette.textSecondary)
}
.padding(ClaudeTheme.Spacing.md)
.background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.danger.opacity(0.08)))
if revealed {
keyBlock(title: "ed_seed", value: identity.edSeedB64)
keyBlock(title: "x_seed", value: identity.xSeedB64)
Button {
UIPasteboard.general.string = "ed_seed=\(identity.edSeedB64)\nx_seed=\(identity.xSeedB64)\nname=\(identity.displayName)"
} label: {
HStack { Image(systemName: "doc.on.doc"); Text("Копировать оба ключа") }
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Capsule().fill(ClaudeTheme.Palette.goldGradient))
}
} else {
Button { revealed = true } label: {
Text("Показать ключи")
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.gold)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.surface))
.overlay(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 1))
}
}
}
.padding(ClaudeTheme.Spacing.lg)
}
.claudeBackground()
.navigationTitle("Экспорт ключей")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Готово") { dismiss() }
.foregroundStyle(ClaudeTheme.Palette.gold)
}
}
}
}
private func keyBlock(title: String, value: String) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.system(size: 11, weight: .semibold))
.tracking(2)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
Text(value)
.font(.system(size: 12, design: .monospaced))
.foregroundStyle(ClaudeTheme.Palette.gold)
.textSelection(.enabled)
.padding(ClaudeTheme.Spacing.md)
.frame(maxWidth: .infinity, alignment: .leading)
.background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.surface))
.overlay(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 0.5))
}
}
}
private struct AboutView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) {
Text("Ӂ")
.font(.system(size: 80, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.goldGradient)
.frame(maxWidth: .infinity)
.padding(.top, ClaudeTheme.Spacing.xl)
VStack(spacing: 4) {
Text("MONTANA")
.font(.system(size: 28, weight: .bold, design: .serif))
.tracking(8)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
Text("MESSENGER")
.font(.system(size: 11, weight: .medium))
.tracking(4)
.foregroundStyle(ClaudeTheme.Palette.gold)
}
.frame(maxWidth: .infinity)
VStack(alignment: .leading, spacing: 12) {
Text("Цифровая собственность.")
Text("Постквантовая криптография.")
Text("Твой ключ — твоя идентичность.")
Text("Никто не читает твои сообщения. Никто не знает, кто ты. Это нормально.")
.padding(.top, 8)
.foregroundStyle(ClaudeTheme.Palette.textSecondary)
}
.font(ClaudeTheme.Typography.serif)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
.padding(.horizontal, ClaudeTheme.Spacing.xl)
.padding(.top, ClaudeTheme.Spacing.lg)
Spacer(minLength: 40)
VStack(spacing: 4) {
Text("XXIIVIIIMMXXXI")
.font(.system(size: 11, design: .serif))
.tracking(2)
.foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.6))
Text("Genesis · 9.01.2026")
.font(.system(size: 10))
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
}
.frame(maxWidth: .infinity)
.padding(.bottom, ClaudeTheme.Spacing.xl)
}
}
.claudeBackground()
.navigationTitle("Манифест")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Готово") { dismiss() }
.foregroundStyle(ClaudeTheme.Palette.gold)
}
}
}
}
}