iOS new: ContactsView.swift

This commit is contained in:
efir369999 2026-05-05 17:16:22 +03:00
parent 23be15b53d
commit 5bafc410b2

View File

@ -0,0 +1,186 @@
import SwiftUI
struct ContactsView: View {
@StateObject private var contacts = ContactsStore.shared
@State private var showAdd = false
@State private var openChat: Contact?
@State private var search = ""
var body: some View {
NavigationStack {
ZStack(alignment: .bottomTrailing) {
Group {
if contacts.contacts.isEmpty {
emptyState
} else {
list
}
}
.claudeBackground()
Button { showAdd = true } label: {
Image(systemName: "person.badge.plus")
.font(.system(size: 22, weight: .medium))
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.frame(width: 56, height: 56)
.background(Circle().fill(ClaudeTheme.Palette.goldGradient))
.shadow(color: ClaudeTheme.Palette.gold.opacity(0.45), radius: 16, y: 4)
}
.padding(.trailing, ClaudeTheme.Spacing.lg)
.padding(.bottom, ClaudeTheme.Spacing.lg)
}
.navigationTitle("Контакты")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showAdd) { AddContactView() }
.navigationDestination(item: $openChat) { c in
ChatDetailView(contact: c)
}
}
}
private var emptyState: some View {
VStack(spacing: ClaudeTheme.Spacing.lg) {
Spacer()
Image(systemName: "person.crop.circle.badge.plus")
.font(.system(size: 56))
.foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.7))
VStack(spacing: 6) {
Text("Контактов пока нет")
.font(ClaudeTheme.Typography.title)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
Text("Добавь собеседника по Account ID, чтобы начать.")
.font(ClaudeTheme.Typography.callout)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private var list: some View {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(filtered) { c in
Button { openChat = c } label: {
HStack(spacing: 12) {
AvatarView(name: c.name, size: 48)
VStack(alignment: .leading, spacing: 2) {
Text(c.name)
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
Text(c.accountID)
.font(ClaudeTheme.Typography.mono)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
}
.padding(.horizontal, ClaudeTheme.Spacing.lg)
.padding(.vertical, ClaudeTheme.Spacing.md)
.contentShape(Rectangle())
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
contacts.delete(c.accountID)
} label: {
Label("Удалить", systemImage: "trash")
}
}
Rectangle().fill(ClaudeTheme.Palette.divider)
.frame(height: 0.5)
.padding(.leading, 76)
}
}
}
.searchable(text: $search, prompt: "Поиск")
}
private var filtered: [Contact] {
guard !search.trimmingCharacters(in: .whitespaces).isEmpty else { return contacts.contacts }
return contacts.contacts.filter {
$0.name.localizedCaseInsensitiveContains(search) ||
$0.accountID.localizedCaseInsensitiveContains(search)
}
}
}
private struct AddContactView: View {
@Environment(\.dismiss) private var dismiss
@State private var input = ""
@State private var error: String?
@State private var working = false
@FocusState private var focused: Bool
var body: some View {
NavigationStack {
VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) {
Text("Account ID собеседника")
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
.padding(.top, ClaudeTheme.Spacing.md)
TextField("например, 7a3f9b1c8d4e2f0a", text: $input)
.font(ClaudeTheme.Typography.mono)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
.focused($focused)
.autocapitalization(.none)
.disableAutocorrection(true)
.padding(14)
.background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.surface))
.overlay(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).strokeBorder(focused ? ClaudeTheme.Palette.gold : ClaudeTheme.Palette.goldHairline, lineWidth: 1))
if let error {
Text(error)
.font(ClaudeTheme.Typography.caption)
.foregroundStyle(ClaudeTheme.Palette.danger)
}
Button { Task { await add() } } label: {
Text(working ? "Добавление..." : "Добавить")
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(input.isEmpty
? AnyShapeStyle(ClaudeTheme.Palette.surface)
: AnyShapeStyle(ClaudeTheme.Palette.goldGradient))
)
}
.disabled(input.isEmpty || working)
Spacer()
}
.padding(.horizontal, ClaudeTheme.Spacing.lg)
.claudeBackground()
.navigationTitle("Новый контакт")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Отмена") { dismiss() }
.foregroundStyle(ClaudeTheme.Palette.textSecondary)
}
}
.toolbarBackground(ClaudeTheme.Palette.canvas, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.onAppear { focused = true }
}
}
@MainActor
private func add() async {
working = true; error = nil
defer { working = false }
do {
_ = try await ContactsStore.shared.addByID(input)
dismiss()
} catch {
self.error = error.localizedDescription
}
}
}