diff --git a/CLAUDE.md b/CLAUDE.md index cf40af9..f784e4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/MarkdownViewer/ContentView.swift b/MarkdownViewer/ContentView.swift index d50661d..201ee0e 100644 --- a/MarkdownViewer/ContentView.swift +++ b/MarkdownViewer/ContentView.swift @@ -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)") } } diff --git a/MarkdownViewer/MarkdownViewerApp.swift b/MarkdownViewer/MarkdownViewerApp.swift index af5b3a5..ba1b5ca 100644 --- a/MarkdownViewer/MarkdownViewerApp.swift +++ b/MarkdownViewer/MarkdownViewerApp.swift @@ -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()