montana/Montana-iOS/Montana Messenger/Montana Messenger/SettingsView.swift

463 lines
21 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import Combine
struct SettingsView: View {
@EnvironmentObject private var identity: IdentityManager
@State private var showAccountID = 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 {
NavigationStack {
ScrollView {
VStack(spacing: ClaudeTheme.Spacing.xl) {
if let id = identity.identity {
ProfileHeader(identity: id, onEditName: {
nameInput = id.displayName
editingName = true
})
.padding(.top, ClaudeTheme.Spacing.xl)
SettingsSection(title: "Идентичность") {
SettingsRow(icon: "person.text.rectangle.fill",
title: "Account ID",
subtitle: id.accountID) {
showAccountID = true
}
SettingsRow(icon: "key.fill",
title: "Экспорт ключей",
subtitle: "только на новое устройство") {
showExport = true
}
}
SettingsSection(title: "Сеть") {
SettingsInfo(icon: "antenna.radiowaves.left.and.right",
title: "Сервер",
value: "mess.montana.quest")
SettingsInfo(icon: "shield.lefthalf.filled",
title: "Шифрование",
value: "X25519 + ChaCha20-Poly1305")
}
SettingsSection(title: "О приложении") {
SettingsRow(icon: "doc.text.fill",
title: "Манифест Монтаны") { showAbout = true }
SettingsInfo(icon: "info.circle.fill",
title: "Версия",
value: "1.0.0 (1)")
}
Button(role: .destructive) { showWipe = true } label: {
Text("Стереть идентичность")
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.danger)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(ClaudeTheme.Palette.surface)
)
.overlay(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.strokeBorder(ClaudeTheme.Palette.danger.opacity(0.3), lineWidth: 0.5)
)
}
.padding(.horizontal, ClaudeTheme.Spacing.lg)
Footer().padding(.bottom, ClaudeTheme.Spacing.xxl)
}
}
}
.claudeBackground()
.navigationTitle("Настройки")
.navigationBarTitleDisplayMode(.inline)
.alert("Стереть идентичность?", isPresented: $showWipe) {
Button("Отмена", role: .cancel) {}
Button("Стереть", role: .destructive) { identity.wipe() }
} message: {
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) }
}
}
}
}
private struct ProfileHeader: View {
let identity: Identity
let onEditName: () -> Void
var body: some View {
VStack(spacing: ClaudeTheme.Spacing.md) {
AvatarView(name: identity.displayName, size: 96)
.shadow(color: ClaudeTheme.Palette.gold.opacity(0.35), radius: 18, y: 4)
Button(action: onEditName) {
HStack(spacing: 6) {
Text(identity.displayName)
.font(.system(size: 24, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
Image(systemName: "pencil")
.font(.system(size: 12))
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
}
}
Text(identity.accountID)
.font(ClaudeTheme.Typography.mono)
.foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.7))
}
.frame(maxWidth: .infinity)
}
}
private struct Footer: View {
var body: some View {
VStack(spacing: 6) {
Text("Ӂ")
.font(.system(size: 36, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.goldGradient)
Text("MONTANA")
.font(.system(size: 10, weight: .medium))
.tracking(3)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
Text("XXIIVIIIMMXXXI")
.font(.system(size: 9, design: .serif))
.tracking(2)
.foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.45))
}
.padding(.top, ClaudeTheme.Spacing.lg)
}
}
private struct SettingsSection<Content: View>: View {
let title: String
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.sm) {
Text(title.uppercased())
.font(.system(size: 11, weight: .semibold))
.tracking(2)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
.padding(.horizontal, ClaudeTheme.Spacing.lg + 4)
VStack(spacing: 0) { content }
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(ClaudeTheme.Palette.surface)
)
.overlay(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 0.5)
)
.padding(.horizontal, ClaudeTheme.Spacing.lg)
}
}
}
private struct SettingsRow: View {
let icon: String
let title: String
var subtitle: String? = nil
let action: () -> Void
var body: some View {
Button(action: action) { rowBody(showChevron: true) }
}
private func rowBody(showChevron: Bool) -> some View {
HStack(spacing: ClaudeTheme.Spacing.md) {
Image(systemName: icon)
.font(.system(size: 14))
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.frame(width: 28, height: 28)
.background(
RoundedRectangle(cornerRadius: 7, style: .continuous)
.fill(ClaudeTheme.Palette.goldGradient)
)
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(ClaudeTheme.Typography.body)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
if let subtitle {
Text(subtitle)
.font(ClaudeTheme.Typography.caption)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
.lineLimit(1)
}
}
Spacer()
if showChevron {
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
}
}
.padding(.horizontal, ClaudeTheme.Spacing.md)
.padding(.vertical, ClaudeTheme.Spacing.md)
.overlay(alignment: .bottom) {
Rectangle().fill(ClaudeTheme.Palette.divider).frame(height: 0.5).padding(.leading, 56)
}
.contentShape(Rectangle())
}
}
private struct SettingsInfo: View {
let icon: String
let title: String
let value: String
var body: some View {
HStack(spacing: ClaudeTheme.Spacing.md) {
Image(systemName: icon)
.font(.system(size: 14))
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.frame(width: 28, height: 28)
.background(
RoundedRectangle(cornerRadius: 7, style: .continuous)
.fill(ClaudeTheme.Palette.goldGradient)
)
Text(title)
.font(ClaudeTheme.Typography.body)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
Spacer()
Text(value)
.font(ClaudeTheme.Typography.caption)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
}
.padding(.horizontal, ClaudeTheme.Spacing.md)
.padding(.vertical, ClaudeTheme.Spacing.md)
.overlay(alignment: .bottom) {
Rectangle().fill(ClaudeTheme.Palette.divider).frame(height: 0.5).padding(.leading, 56)
}
}
}
private struct AccountIDView: View {
@Environment(\.dismiss) private var dismiss
let identity: Identity
var body: some View {
NavigationStack {
VStack(spacing: ClaudeTheme.Spacing.xl) {
AvatarView(name: identity.displayName, size: 120)
.padding(.top, ClaudeTheme.Spacing.xxl)
.shadow(color: ClaudeTheme.Palette.gold.opacity(0.4), radius: 22, y: 6)
Text(identity.displayName)
.font(ClaudeTheme.Typography.title)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
VStack(spacing: ClaudeTheme.Spacing.sm) {
Text("Account ID")
.font(.system(size: 11, weight: .medium))
.tracking(2)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
Text(identity.accountID)
.font(.system(size: 18, design: .monospaced))
.foregroundStyle(ClaudeTheme.Palette.gold)
.padding(ClaudeTheme.Spacing.md)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(ClaudeTheme.Palette.surface)
)
.overlay(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 0.5)
)
}
.padding(.horizontal, ClaudeTheme.Spacing.lg)
Text("Поделись этим Account ID с тем, кто хочет тебе написать.")
.font(ClaudeTheme.Typography.caption)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
.multilineTextAlignment(.center)
.padding(.horizontal, ClaudeTheme.Spacing.xl)
Button {
UIPasteboard.general.string = identity.accountID
} label: {
HStack {
Image(systemName: "doc.on.doc")
Text("Копировать")
}
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.padding(.horizontal, ClaudeTheme.Spacing.xl)
.padding(.vertical, 12)
.background(Capsule().fill(ClaudeTheme.Palette.goldGradient))
}
Spacer()
}
.claudeBackground()
.navigationTitle("Account ID")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Готово") { dismiss() }
.foregroundStyle(ClaudeTheme.Palette.gold)
}
}
}
}
}
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)
}
}
}
}
}