iOS new: ContactsView.swift
This commit is contained in:
parent
23be15b53d
commit
5bafc410b2
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user