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:
@@ -41,6 +41,7 @@ This repository contains a macOS markdown viewer application built with SwiftUI.
|
||||
- [ ] Search within document
|
||||
- [ ] Zoom in/out functionality
|
||||
- [x] Recent files menu
|
||||
- [x] Save functionality via File menu (Save/Save As with ⌘S/⌘⇧S shortcuts)
|
||||
- [ ] Drag and drop file support
|
||||
- [ ] Support for math equations (LaTeX)
|
||||
- [ ] Image paste support
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
@@ -23,14 +24,18 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
struct MarkdownDocumentView: View {
|
||||
let content: String
|
||||
let originalContent: String
|
||||
let fileName: String
|
||||
let fileURL: URL?
|
||||
@State private var isEditMode = false
|
||||
@State private var editableContent: String
|
||||
@State private var hasUnsavedChanges = false
|
||||
@State private var isSaving = false
|
||||
|
||||
init(content: String, fileName: String) {
|
||||
self.content = content
|
||||
init(content: String, fileName: String, fileURL: URL? = nil) {
|
||||
self.originalContent = content
|
||||
self.fileName = fileName
|
||||
self.fileURL = fileURL
|
||||
self._editableContent = State(initialValue: content)
|
||||
print("MarkdownDocumentView: Initialized with content length: \(content.count)")
|
||||
}
|
||||
@@ -49,12 +54,6 @@ struct MarkdownDocumentView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if isEditMode {
|
||||
Button("Save") {
|
||||
// TODO: Implement save functionality
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
@@ -91,7 +90,80 @@ struct MarkdownDocumentView: View {
|
||||
.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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@ import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import AppKit
|
||||
|
||||
extension Notification.Name {
|
||||
static let saveDocument = Notification.Name("saveDocument")
|
||||
static let saveDocumentAs = Notification.Name("saveDocumentAs")
|
||||
}
|
||||
|
||||
@main
|
||||
struct MarkdownViewerApp: App {
|
||||
@StateObject private var recentFilesManager = RecentFilesManager.shared
|
||||
@@ -19,6 +24,18 @@ struct MarkdownViewerApp: App {
|
||||
|
||||
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") {
|
||||
if recentFilesManager.recentFiles.isEmpty {
|
||||
Text("No Recent Files")
|
||||
@@ -91,7 +108,8 @@ struct MarkdownViewerApp: App {
|
||||
newWindow.contentView = NSHostingView(
|
||||
rootView: MarkdownDocumentView(
|
||||
content: content,
|
||||
fileName: url.lastPathComponent
|
||||
fileName: url.lastPathComponent,
|
||||
fileURL: url
|
||||
)
|
||||
)
|
||||
newWindow.center()
|
||||
|
||||
Reference in New Issue
Block a user