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
|
- [ ] 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
|
||||||
|
|||||||
@@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user