463 lines
21 KiB
Swift
463 lines
21 KiB
Swift
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|