Move save functionality to File menu

- Remove save button from application toolbar
- Add Save (⌘S) and Save As (⌘⇧S) menu items to File menu
- Implement notification-based communication between menu and document views
- Add unsaved changes indicator in window titles
- Support both saving existing files and creating new files

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
0x8664b2
2025-06-20 11:03:18 -07:00
parent a3e8b09a8f
commit 68d6dd906f
3 changed files with 102 additions and 11 deletions

View File

@@ -41,6 +41,7 @@ This repository contains a macOS markdown viewer application built with SwiftUI.
- [ ] Search within document - [ ] Search within document
- [ ] Zoom in/out functionality - [ ] Zoom in/out functionality
- [x] Recent files menu - [x] Recent files menu
- [x] Save functionality via File menu (Save/Save As with ⌘S/⌘⇧S shortcuts)
- [ ] Drag and drop file support - [ ] Drag and drop file support
- [ ] Support for math equations (LaTeX) - [ ] Support for math equations (LaTeX)
- [ ] Image paste support - [ ] Image paste support

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
import UniformTypeIdentifiers
struct ContentView: View { struct ContentView: View {
var body: some View { var body: some View {
@@ -23,14 +24,18 @@ struct ContentView: View {
} }
struct MarkdownDocumentView: View { struct MarkdownDocumentView: View {
let content: String let originalContent: String
let fileName: String let fileName: String
let fileURL: URL?
@State private var isEditMode = false @State private var isEditMode = false
@State private var editableContent: String @State private var editableContent: String
@State private var hasUnsavedChanges = false
@State private var isSaving = false
init(content: String, fileName: String) { init(content: String, fileName: String, fileURL: URL? = nil) {
self.content = content self.originalContent = content
self.fileName = fileName self.fileName = fileName
self.fileURL = fileURL
self._editableContent = State(initialValue: content) self._editableContent = State(initialValue: content)
print("MarkdownDocumentView: Initialized with content length: \(content.count)") print("MarkdownDocumentView: Initialized with content length: \(content.count)")
} }
@@ -49,12 +54,6 @@ struct MarkdownDocumentView: View {
Spacer() Spacer()
if isEditMode {
Button("Save") {
// TODO: Implement save functionality
}
.buttonStyle(.borderedProminent)
}
} }
.padding() .padding()
.background(Color(NSColor.controlBackgroundColor)) .background(Color(NSColor.controlBackgroundColor))
@@ -91,7 +90,80 @@ struct MarkdownDocumentView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
} }
} }
.navigationTitle(fileName) .navigationTitle(hasUnsavedChanges ? "\(fileName) • Edited" : fileName)
.onChange(of: editableContent) { newValue in
hasUnsavedChanges = (newValue != originalContent)
}
.onReceive(NotificationCenter.default.publisher(for: .saveDocument)) { _ in
saveFile()
}
.onReceive(NotificationCenter.default.publisher(for: .saveDocumentAs)) { _ in
saveAsNewFile()
}
}
func saveFile() {
guard let fileURL = fileURL else {
// If no URL, show save dialog
saveAsNewFile()
return
}
guard hasUnsavedChanges else { return }
isSaving = true
Task {
do {
try editableContent.write(to: fileURL, atomically: true, encoding: .utf8)
await MainActor.run {
hasUnsavedChanges = false
isSaving = false
}
} catch {
await MainActor.run {
isSaving = false
showSaveError(error)
}
}
}
}
func saveAsNewFile() {
let panel = NSSavePanel()
if let mdType = UTType(filenameExtension: "md") {
panel.allowedContentTypes = [mdType]
} else {
panel.allowedContentTypes = [.plainText]
}
panel.nameFieldStringValue = fileName
if panel.runModal() == .OK, let url = panel.url {
isSaving = true
Task {
do {
try editableContent.write(to: url, atomically: true, encoding: .utf8)
await MainActor.run {
hasUnsavedChanges = false
isSaving = false
}
} catch {
await MainActor.run {
isSaving = false
showSaveError(error)
}
}
}
}
}
private func showSaveError(_ error: Error) {
// For now, just print the error
// In a full implementation, we'd show an alert dialog
print("Save error: \(error.localizedDescription)")
} }
} }

View File

@@ -2,6 +2,11 @@ import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
import AppKit import AppKit
extension Notification.Name {
static let saveDocument = Notification.Name("saveDocument")
static let saveDocumentAs = Notification.Name("saveDocumentAs")
}
@main @main
struct MarkdownViewerApp: App { struct MarkdownViewerApp: App {
@StateObject private var recentFilesManager = RecentFilesManager.shared @StateObject private var recentFilesManager = RecentFilesManager.shared
@@ -19,6 +24,18 @@ struct MarkdownViewerApp: App {
Divider() Divider()
Button("Save") {
NotificationCenter.default.post(name: .saveDocument, object: nil)
}
.keyboardShortcut("s", modifiers: .command)
Button("Save As...") {
NotificationCenter.default.post(name: .saveDocumentAs, object: nil)
}
.keyboardShortcut("s", modifiers: [.command, .shift])
Divider()
Menu("Recent Files") { Menu("Recent Files") {
if recentFilesManager.recentFiles.isEmpty { if recentFilesManager.recentFiles.isEmpty {
Text("No Recent Files") Text("No Recent Files")
@@ -91,7 +108,8 @@ struct MarkdownViewerApp: App {
newWindow.contentView = NSHostingView( newWindow.contentView = NSHostingView(
rootView: MarkdownDocumentView( rootView: MarkdownDocumentView(
content: content, content: content,
fileName: url.lastPathComponent fileName: url.lastPathComponent,
fileURL: url
) )
) )
newWindow.center() newWindow.center()