Skip to content

Commit cd9a7ef

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 890a9c6 commit cd9a7ef

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
### Improvements
1617
- Deleting an entry inside the Recycle Bin now permanently erases it instead of re-moving it to the same bin

KeeForge/Views/GroupListView.swift

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

0 commit comments

Comments
 (0)