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