feat: add EntryEditView for add/edit entries

Also fixes real bugs found while making Phase 4 build clean on both
platforms:
- FileBookmarkService used .withSecurityScope unconditionally, but
  that bookmark option is macOS-only -- iOS grants scoped access
  automatically once the user picks a file. Now conditional per platform.
- EntryEditViewModel's groupId was non-optional with no default, but
  EntryDetailView's edit-sheet call site (Task 14) has no group
  reference to pass -- only the "add new entry" path actually needs
  it, so groupId is now optional and validated at save time instead.
- Missing `import Combine` in EntryDetailView (Timer.publish/autoconnect)
  and UnlockViewModel/VaultViewModel/EntryEditViewModel (@Published).
- GroupBrowserView's `group: Group` property collided with SwiftUI's
  own Group view type -- qualified as MyPassCore.Group.
- EntryEditView's .keyboardType(.URL) and .navigationBarTitleDisplayMode
  are iOS-only APIs, guarded with #if os(iOS) for the macOS build.

Verified clean (aside from the known, expected ContentView.swift/Item
breakage) on both iOS Simulator and macOS destinations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
This commit is contained in:
2026-09-19 16:00:37 +02:00
co-authored by Claude Sonnet 5
parent 347f9a99c4
commit a48e8baf7a
5 changed files with 181 additions and 4 deletions
@@ -0,0 +1,86 @@
//
// EntryEditViewModel.swift
// MyPass
//
import Foundation
import Combine
import SwiftUI
import MyPassCore
@MainActor
final class EntryEditViewModel: ObservableObject {
@Published var title: String
@Published var username: String
@Published var password: String
@Published var url: String
@Published var notes: String
@Published var customFields: [CustomField]
@Published var errorMessage: String?
@Published var isSaving: Bool = false
private let session: VaultSession
private let groupId: UUID?
private let existingEntry: Entry?
/// `groupId` is required when adding a new entry, and unused when editing an existing one
/// (an existing entry's group is looked up internally by `VaultSession.updateEntry`).
init(session: VaultSession, groupId: UUID? = nil, existing: Entry? = nil) {
self.session = session
self.groupId = groupId
self.existingEntry = existing
title = existing?.title ?? ""
username = existing?.username ?? ""
password = existing?.password.reveal() ?? ""
url = existing?.url ?? ""
notes = existing?.notes ?? ""
customFields = existing?.customFields ?? []
}
var isEditing: Bool { existingEntry != nil }
func save(dismiss: () -> Void) {
Task {
isSaving = true
errorMessage = nil
do {
if var entry = existingEntry {
entry.title = title
entry.username = username
entry.password = ProtectedString(password, isProtected: true)
entry.url = url
entry.notes = notes
entry.customFields = customFields
try session.updateEntry(entry)
} else {
guard let groupId else {
errorMessage = "No group selected for the new entry."
isSaving = false
return
}
let entry = Entry(
title: title,
username: username,
password: ProtectedString(password, isProtected: true),
url: url,
notes: notes,
customFields: customFields
)
try session.addEntry(entry, toGroupId: groupId)
}
dismiss()
} catch {
errorMessage = error.localizedDescription
}
isSaving = false
}
}
func addCustomField() {
customFields.append(CustomField(key: "", value: ProtectedString("", isProtected: false)))
}
func removeCustomField(at offsets: IndexSet) {
customFields.remove(atOffsets: offsets)
}
}