diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 38ad408..2bb11f6 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -1,109 +1,134 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var model: WriteFreelyModel @Binding var sidebarIsHidden: Bool var body: some View { NavigationView { #if os(macOS) SidebarView() .toolbar { Button( action: { NSApp.keyWindow?.contentViewController?.tryToPerform( #selector(NSSplitViewController.toggleSidebar(_:)), with: nil ) withAnimation { self.sidebarIsHidden.toggle() } }, label: { Image(systemName: "sidebar.left") } ) Spacer() - Button(action: {}, label: { Image(systemName: "square.and.pencil") }) + Button(action: { + withAnimation { + self.model.selectedPost = nil + } + let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) + managedPost.createdDate = Date() + managedPost.title = "" + managedPost.body = "" + managedPost.status = PostStatus.local.rawValue + managedPost.collectionAlias = nil + switch model.preferences.font { + case 1: + managedPost.appearance = "sans" + case 2: + managedPost.appearance = "wrap" + default: + managedPost.appearance = "serif" + } + if let languageCode = Locale.current.languageCode { + managedPost.language = languageCode + managedPost.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft + } + withAnimation { + self.model.selectedPost = managedPost + } + }, label: { Image(systemName: "square.and.pencil") }) } #else SidebarView() #endif #if os(macOS) PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn) .toolbar { ToolbarItem(placement: .navigation) { Button(action: {}, label: { Image(systemName: "arrow.clockwise") }) .padding(.leading, sidebarIsHidden ? 8 : 0) .animation(.linear) } ToolbarItem(placement: .status) { if let selectedPost = model.selectedPost { PostStatusBadgeView(post: selectedPost) } } ToolbarItemGroup(placement: .primaryAction) { if let selectedPost = model.selectedPost { Button(action: {}, label: { Image(systemName: "paperplane") }) .disabled(selectedPost.body.isEmpty) Button(action: {}, label: { Image(systemName: "square.and.arrow.up") }) .disabled(selectedPost.status == PostStatus.local.rawValue) } } } #else PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn) #endif Text("Select a post, or create a new local draft.") .foregroundColor(.secondary) } .environmentObject(model) .alert(isPresented: $model.isPresentingDeleteAlert) { Alert( title: Text("Delete Post?"), message: Text("This action cannot be undone."), primaryButton: .destructive(Text("Delete"), action: { if let postToDelete = model.postToDelete { model.selectedPost = nil DispatchQueue.main.async { model.posts.remove(postToDelete) } model.postToDelete = nil } }), secondaryButton: .cancel() { model.postToDelete = nil } ) } .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { Alert( title: Text("Connection Error"), message: Text("There is no internet connection at the moment. Please reconnect or try again later"), dismissButton: .default(Text("OK"), action: { model.isPresentingNetworkErrorAlert = false }) ) }) #if os(iOS) EmptyView() .sheet( isPresented: $model.isPresentingSettingsView, onDismiss: { model.isPresentingSettingsView = false }, content: { SettingsView() .environmentObject(model) } ) #endif } } struct ContentView_Previews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let model = WriteFreelyModel() return ContentView(sidebarIsHidden: .constant(false)) .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift index 98e158b..c7ada24 100644 --- a/Shared/PostList/PostListModel.swift +++ b/Shared/PostList/PostListModel.swift @@ -1,124 +1,126 @@ import SwiftUI import CoreData class PostListModel: ObservableObject { func remove(_ post: WFAPost) { - LocalStorageManager.persistentContainer.viewContext.delete(post) - LocalStorageManager().saveContext() + withAnimation { + LocalStorageManager.persistentContainer.viewContext.delete(post) + LocalStorageManager().saveContext() + } } func purgePublishedPosts() { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "WFAPost") fetchRequest.predicate = NSPredicate(format: "status != %i", 0) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { try LocalStorageManager.persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { print("Error: Failed to purge cached posts.") } } func getBodyPreview(of post: WFAPost) -> String { var elidedPostBody: String = "" // Strip any markdown from the post body. let strippedPostBody = stripMarkdown(from: post.body) // Extract lede from post. elidedPostBody = extractLede(from: strippedPostBody) return elidedPostBody } } private extension PostListModel { func stripMarkdown(from string: String) -> String { var strippedString = string strippedString = stripHeadingOctothorpes(from: strippedString) strippedString = stripImages(from: strippedString, keepAltText: true) return strippedString } func stripHeadingOctothorpes(from string: String) -> String { let newLines = CharacterSet.newlines var processedComponents: [String] = [] let components = string.components(separatedBy: newLines) for component in components { if component.isEmpty { continue } var newString = component while newString.first == "#" { newString.removeFirst() } if newString.hasPrefix(" ") { newString.removeFirst() } processedComponents.append(newString) } let headinglessString = processedComponents.joined(separator: "\n\n") return headinglessString } func stripImages(from string: String, keepAltText: Bool = false) -> String { let pattern = #"!\[[\"]?(.*?)[\"|]?\]\(.*?\)"# var processedComponents: [String] = [] let components = string.components(separatedBy: .newlines) for component in components { if component.isEmpty { continue } var processedString: String = component if keepAltText { let regex = try? NSRegularExpression(pattern: pattern, options: []) if let matches = regex?.matches( in: component, options: [], range: NSRange(location: 0, length: component.utf16.count) ) { for match in matches { if let range = Range(match.range(at: 1), in: component) { processedString = "\(component[range])" } } } } else { let range = component.startIndex.. String { let truncatedString = string.prefix(80) let terminatingPunctuation = ".。?" let terminatingCharacters = CharacterSet(charactersIn: terminatingPunctuation).union(.newlines) var lede: String = "" let sentences = truncatedString.components(separatedBy: terminatingCharacters) if let firstSentence = (sentences.filter { !$0.isEmpty }).first { if truncatedString.count > firstSentence.count { if terminatingPunctuation.contains(truncatedString[firstSentence.endIndex]) { lede = String(truncatedString[...firstSentence.endIndex]) } else { lede = firstSentence } } else if truncatedString.count == firstSentence.count { if string.count > 80 { if let endOfStringIndex = truncatedString.lastIndex(of: " ") { lede = truncatedString[..