Skip to content

Commit 960e7cb

Browse files
crazytanclaude
andcommitted
Open database-specific settings from unlocked database view
The gear button in the unlocked database toolbar now opens a DatabaseSettingsView with nickname, read-only, key file, metadata, and cloud sync details instead of the global app settings sheet. App settings remain accessible via a link at the bottom. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4408bb5 commit 960e7cb

2 files changed

Lines changed: 188 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
### New Features
1212
- Add main-app entry editing with create, edit, delete, password generation, save-conflict resolution, and read-only editing safeguards
1313
- Added: Save new credentials and generate strong passwords directly from AutoFill, with offline-safe queueing for Dropbox-backed databases.
14+
- Settings button in unlocked database view now opens database-specific settings (nickname, read-only, key file, metadata, cloud sync) with a link to app settings
1415

1516
### Fixes
1617
- Autosave entry create/edit/delete changes immediately after they are staged, remove the confusing unlocked-screen save button, and show retry-only save UI when an autosave fails

KeeForge/Views/GroupListView.swift

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ struct GroupListView: View {
136136
}
137137
}
138138
.sheet(isPresented: $showSettings) {
139-
SettingsView(viewModel: viewModel)
139+
DatabaseSettingsView(viewModel: viewModel)
140140
}
141141
.alert(item: $pendingEntryDeletion) { action in
142142
Alert(
@@ -247,3 +247,189 @@ struct EntryRow: View {
247247
}
248248
}
249249
}
250+
251+
struct DatabaseSettingsView: View {
252+
@Bindable var viewModel: DatabaseViewModel
253+
@Environment(\.dismiss) private var dismiss
254+
@State private var nickname = ""
255+
@State private var isQuickLaunch = false
256+
@State private var showKeyFilePicker = false
257+
@State private var showAppSettings = false
258+
259+
private var reference: DatabaseReference {
260+
viewModel.databaseReference
261+
}
262+
263+
private var currentReference: DatabaseReference {
264+
DatabaseListStore.databases.first(where: { $0.id == reference.id }) ?? reference
265+
}
266+
267+
var body: some View {
268+
NavigationStack {
269+
Form {
270+
Section {
271+
LabeledContent("Name", value: currentDisplayName)
272+
273+
LabeledContent("Custom Name") {
274+
TextField("Use filename", text: $nickname)
275+
.multilineTextAlignment(.trailing)
276+
.onSubmit(saveNickname)
277+
}
278+
279+
LabeledContent("Filename", value: reference.filename)
280+
281+
Toggle("Quick Launch", isOn: $isQuickLaunch)
282+
.onChange(of: isQuickLaunch) { _, newValue in
283+
toggleQuickLaunch(newValue)
284+
}
285+
} header: {
286+
Text("Identity")
287+
} footer: {
288+
Text("Quick Launch opens this database automatically on app launch.")
289+
}
290+
291+
Section {
292+
Toggle(
293+
"Read-only",
294+
isOn: Binding(
295+
get: { currentReference.isReadOnly },
296+
set: { DatabaseListStore.setReadOnly($0, for: reference) }
297+
)
298+
)
299+
.accessibilityIdentifier("database-settings.read-only-toggle")
300+
} header: {
301+
Text("Editing")
302+
} footer: {
303+
Text("Keep this database openable but block create, edit, and delete actions until you turn editing back on.")
304+
}
305+
306+
Section("Key File") {
307+
LabeledContent("Associated File", value: currentReference.keyFileFilename ?? "None")
308+
309+
Button("Select Key File") {
310+
showKeyFilePicker = true
311+
}
312+
313+
if currentReference.keyFileFilename != nil {
314+
Button("Clear Key File", role: .destructive) {
315+
setKeyFile(url: nil)
316+
}
317+
}
318+
}
319+
320+
Section("Metadata") {
321+
LabeledContent("Added", value: dateText(reference.addedAt))
322+
323+
if let lastOpenedAt = reference.lastOpenedAt {
324+
LabeledContent("Last Opened", value: dateText(lastOpenedAt))
325+
}
326+
}
327+
328+
if let metadata = currentReference.cloudSyncMetadata {
329+
Section {
330+
LabeledContent("Provider") {
331+
HStack(spacing: 6) {
332+
CloudProviderIcon(provider: metadata.providerKind, size: 16)
333+
Text(metadata.providerKind?.displayName ?? metadata.provider)
334+
}
335+
.lineLimit(1)
336+
}
337+
338+
LabeledContent("Path") {
339+
Text(metadata.displayPath)
340+
.lineLimit(1)
341+
.truncationMode(.middle)
342+
.multilineTextAlignment(.trailing)
343+
}
344+
345+
if let remoteModifiedAt = metadata.remoteModifiedAt {
346+
LabeledContent("Remote Modified", value: dateText(remoteModifiedAt))
347+
}
348+
349+
if let lastSyncedAt = metadata.lastSyncedAt {
350+
LabeledContent("Last Sync", value: dateText(lastSyncedAt))
351+
}
352+
} header: {
353+
Text("Cloud Sync")
354+
}
355+
}
356+
357+
Section {
358+
Button("App Settings") {
359+
showAppSettings = true
360+
}
361+
}
362+
}
363+
.navigationTitle("Database Settings")
364+
.navigationBarTitleDisplayMode(.inline)
365+
.onAppear {
366+
nickname = currentReference.nickname ?? ""
367+
isQuickLaunch = currentReference.isQuickLaunch
368+
}
369+
.toolbar {
370+
ToolbarItem(placement: .cancellationAction) {
371+
Button("Close") {
372+
saveNickname()
373+
dismiss()
374+
}
375+
}
376+
}
377+
.fileImporter(
378+
isPresented: $showKeyFilePicker,
379+
allowedContentTypes: [.data],
380+
allowsMultipleSelection: false
381+
) { result in
382+
if case .success(let urls) = result, let url = urls.first {
383+
setKeyFile(url: url)
384+
}
385+
}
386+
.sheet(isPresented: $showAppSettings) {
387+
SettingsView(viewModel: viewModel)
388+
}
389+
}
390+
}
391+
392+
private func saveNickname() {
393+
let trimmed = nickname.trimmingCharacters(in: .whitespacesAndNewlines)
394+
let newNickname = trimmed.isEmpty ? nil : trimmed
395+
guard var updated = DatabaseListStore.databases.first(where: { $0.id == reference.id }) else { return }
396+
updated.nickname = newNickname
397+
DatabaseListStore.update(updated)
398+
}
399+
400+
private func toggleQuickLaunch(_ newValue: Bool) {
401+
// Clear Quick Launch from any other database first
402+
if newValue {
403+
for database in DatabaseListStore.databases where database.id != reference.id && database.isQuickLaunch {
404+
var updated = database
405+
updated.isQuickLaunch = false
406+
DatabaseListStore.update(updated)
407+
}
408+
}
409+
guard var updated = DatabaseListStore.databases.first(where: { $0.id == reference.id }) else { return }
410+
updated.isQuickLaunch = newValue
411+
DatabaseListStore.update(updated)
412+
}
413+
414+
private func setKeyFile(url: URL?) {
415+
guard var updated = DatabaseListStore.databases.first(where: { $0.id == reference.id }) else { return }
416+
if let url {
417+
guard let bookmarkData = try? SecurityScopedBookmarkManager.makeBookmarkData(for: url) else { return }
418+
updated.keyFileBookmarkData = bookmarkData
419+
updated.keyFileFilename = url.lastPathComponent
420+
} else {
421+
updated.keyFileBookmarkData = nil
422+
updated.keyFileFilename = nil
423+
}
424+
DatabaseListStore.update(updated)
425+
}
426+
427+
private var currentDisplayName: String {
428+
let trimmed = nickname.trimmingCharacters(in: .whitespacesAndNewlines)
429+
return trimmed.isEmpty ? reference.displayName : trimmed
430+
}
431+
432+
private func dateText(_ date: Date) -> String {
433+
date.formatted(date: .abbreviated, time: .shortened)
434+
}
435+
}

0 commit comments

Comments
 (0)