164 lines
5.8 KiB
Swift
164 lines
5.8 KiB
Swift
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
|
|
|
|
var body: some Scene {
|
|
Settings {
|
|
EmptyView()
|
|
}
|
|
.commands {
|
|
CommandGroup(replacing: .newItem) {
|
|
Button("Open Markdown File...") {
|
|
openMarkdownFile()
|
|
}
|
|
.keyboardShortcut("o", modifiers: .command)
|
|
|
|
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")
|
|
.disabled(true)
|
|
} else {
|
|
ForEach(recentFilesManager.recentFiles, id: \.self) { url in
|
|
Button(action: {
|
|
openRecentFile(url)
|
|
}) {
|
|
VStack(alignment: .leading) {
|
|
Text(recentFilesManager.getDisplayName(for: url))
|
|
Text(recentFilesManager.getRelativePath(for: url))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
Divider()
|
|
|
|
Button("Clear Recent Files") {
|
|
recentFilesManager.clearRecentFiles()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CommandGroup(replacing: .undoRedo) {
|
|
Button("Undo") {
|
|
print("Menu: Undo clicked, sending action to: \(NSApp.keyWindow?.firstResponder?.className ?? "nil")")
|
|
NSApp.sendAction(#selector(UndoManager.undo), to: nil, from: nil)
|
|
}
|
|
.keyboardShortcut("z", modifiers: .command)
|
|
|
|
Button("Redo") {
|
|
print("Menu: Redo clicked, sending action to: \(NSApp.keyWindow?.firstResponder?.className ?? "nil")")
|
|
NSApp.sendAction(#selector(UndoManager.redo), to: nil, from: nil)
|
|
}
|
|
.keyboardShortcut("z", modifiers: [.command, .shift])
|
|
}
|
|
}
|
|
}
|
|
|
|
private func openMarkdownFile() {
|
|
let panel = NSOpenPanel()
|
|
panel.allowsMultipleSelection = true
|
|
panel.canChooseDirectories = false
|
|
panel.canChooseFiles = true
|
|
panel.allowedContentTypes = [UTType.plainText, UTType(filenameExtension: "md")!]
|
|
|
|
if panel.runModal() == .OK {
|
|
for url in panel.urls {
|
|
openNewWindow(for: url)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func openRecentFile(_ url: URL) {
|
|
// Check if file still exists
|
|
guard FileManager.default.fileExists(atPath: url.path) else {
|
|
recentFilesManager.removeRecentFile(url)
|
|
showErrorWindow(message: "File no longer exists", details: url.path)
|
|
return
|
|
}
|
|
|
|
openNewWindow(for: url)
|
|
}
|
|
|
|
private func openNewWindow(for url: URL) {
|
|
do {
|
|
let content = try String(contentsOf: url, encoding: .utf8)
|
|
|
|
// Add to recent files
|
|
recentFilesManager.addRecentFile(url)
|
|
|
|
let newWindow = NSWindow(
|
|
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
|
|
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
|
|
newWindow.title = url.lastPathComponent
|
|
newWindow.contentView = NSHostingView(
|
|
rootView: MarkdownDocumentView(
|
|
content: content,
|
|
fileName: url.lastPathComponent,
|
|
fileURL: url
|
|
)
|
|
)
|
|
newWindow.center()
|
|
newWindow.makeKeyAndOrderFront(nil)
|
|
|
|
} catch {
|
|
print("Error reading file: \(error)")
|
|
showErrorWindow(message: "Error loading file", details: error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
private func showErrorWindow(message: String, details: String) {
|
|
let newWindow = NSWindow(
|
|
contentRect: NSRect(x: 0, y: 0, width: 400, height: 200),
|
|
styleMask: [.titled, .closable],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
|
|
newWindow.title = "Error"
|
|
newWindow.contentView = NSHostingView(
|
|
rootView: VStack {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
.font(.system(size: 40))
|
|
.foregroundColor(.red)
|
|
Text(message)
|
|
.font(.headline)
|
|
Text(details)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.padding()
|
|
)
|
|
newWindow.center()
|
|
newWindow.makeKeyAndOrderFront(nil)
|
|
}
|
|
} |