// // EntryDetailView.swift // MyPass // import SwiftUI import Combine import MyPassCore struct EntryDetailView: View { let entry: Entry let groupId: UUID let session: VaultSession @State private var showPassword = false @State private var showEditSheet = false var body: some View { List { credentialSection if !entry.url.isEmpty { urlSection } if !entry.notes.isEmpty { notesSection } if entry.totp != nil { totpSection } if !entry.customFields.isEmpty { customFieldsSection } if !entry.attachments.isEmpty { attachmentsSection } } .navigationTitle(entry.title) .toolbar { ToolbarItem(placement: .primaryAction) { Button("Edit") { showEditSheet = true } } } .sheet(isPresented: $showEditSheet) { EntryEditView(vm: EntryEditViewModel(session: session, groupId: groupId, existing: entry)) } #if os(macOS) .keyboardShortcut("e", modifiers: .command) #endif } private var credentialSection: some View { Section("Credentials") { FieldRow(label: "Username", value: entry.username, isCopyable: true) HStack { VStack(alignment: .leading, spacing: 2) { Text("Password").font(.caption).foregroundStyle(.secondary) Text(showPassword ? entry.password.reveal() : String(repeating: "•", count: 10)) .font(.body.monospaced()) } Spacer() Button { showPassword.toggle() } label: { Image(systemName: showPassword ? "eye.slash" : "eye") } .buttonStyle(.plain) Button { ClipboardService.copy(entry.password.reveal()) } label: { Image(systemName: "doc.on.doc") } .buttonStyle(.plain) } } } private var urlSection: some View { Section("URL") { FieldRow(label: "URL", value: entry.url, isCopyable: true) } } private var notesSection: some View { Section("Notes") { Text(entry.notes) .font(.body) } } private var totpSection: some View { Section("One-Time Password") { if let config = entry.totp { TOTPRow(config: config) } } } private var customFieldsSection: some View { Section("Custom Fields") { ForEach(entry.customFields) { field in FieldRow( label: field.key, value: field.value.reveal(), isCopyable: true, isProtected: field.value.isProtected ) } } } private var attachmentsSection: some View { Section("Attachments") { ForEach(entry.attachments) { att in Label(att.name, systemImage: "paperclip") } } } } private struct FieldRow: View { let label: String let value: String var isCopyable: Bool = false var isProtected: Bool = false @State private var revealed = false var body: some View { HStack { VStack(alignment: .leading, spacing: 2) { Text(label).font(.caption).foregroundStyle(.secondary) Text(isProtected && !revealed ? "••••••••" : value) .font(.body) } Spacer() if isProtected { Button { revealed.toggle() } label: { Image(systemName: revealed ? "eye.slash" : "eye") }.buttonStyle(.plain) } if isCopyable { Button { ClipboardService.copy(value) } label: { Image(systemName: "doc.on.doc") }.buttonStyle(.plain) } } } } private struct TOTPRow: View { let config: TOTPConfig @State private var code: String = "" @State private var secondsLeft: Int = 30 let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { HStack { VStack(alignment: .leading, spacing: 2) { Text("TOTP").font(.caption).foregroundStyle(.secondary) Text(formattedCode).font(.title3.monospaced()).bold() } Spacer() ZStack { Circle() .stroke(Color.secondary.opacity(0.2), lineWidth: 3) Circle() .trim(from: 0, to: CGFloat(secondsLeft) / CGFloat(config.period)) .stroke(secondsLeft > 10 ? Color.green : Color.orange, lineWidth: 3) .rotationEffect(.degrees(-90)) .animation(.linear(duration: 1), value: secondsLeft) Text("\(secondsLeft)").font(.caption2) } .frame(width: 32, height: 32) Button { ClipboardService.copy(code) } label: { Image(systemName: "doc.on.doc") }.buttonStyle(.plain) } .onReceive(timer) { _ in refresh() } .onAppear { refresh() } } private var formattedCode: String { guard code.count == 6 else { return code } return String(code.prefix(3)) + " " + String(code.suffix(3)) } private func refresh() { code = TOTPGenerator.generate(config: config) secondsLeft = TOTPGenerator.secondsRemaining(config: config) } }