diff --git a/Shared/Account/AccountModel.swift b/Shared/Account/AccountModel.swift index 4ce81b2..25683a5 100644 --- a/Shared/Account/AccountModel.swift +++ b/Shared/Account/AccountModel.swift @@ -1,4 +1,4 @@ -import Foundation +import SwiftUI import WriteFreely enum AccountError: Error { @@ -30,8 +30,8 @@ extension AccountError: LocalizedError { } struct AccountModel { + @AppStorage("isLoggedIn") var isLoggedIn: Bool = false private let defaults = UserDefaults.standard - let isLoggedInFlag = "isLoggedInFlag" let usernameStringKey = "usernameStringKey" let serverStringKey = "serverStringKey" @@ -39,13 +39,11 @@ struct AccountModel { var username: String = "" private(set) var user: WFUser? - private(set) var isLoggedIn: Bool = false mutating func login(_ user: WFUser) { self.user = user self.username = user.username ?? "" self.isLoggedIn = true - defaults.set(true, forKey: isLoggedInFlag) defaults.set(user.username, forKey: usernameStringKey) defaults.set(server, forKey: serverStringKey) } @@ -53,13 +51,11 @@ struct AccountModel { mutating func logout() { self.user = nil self.isLoggedIn = false - defaults.set(false, forKey: isLoggedInFlag) defaults.removeObject(forKey: usernameStringKey) defaults.removeObject(forKey: serverStringKey) } mutating func restoreState() { - isLoggedIn = defaults.bool(forKey: isLoggedInFlag) server = defaults.string(forKey: serverStringKey) ?? "" username = defaults.string(forKey: usernameStringKey) ?? "" } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 6831b7c..ce8f253 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -5,41 +5,79 @@ struct ContentView: View { var body: some View { NavigationView { + #if os(macOS) SidebarView() + .toolbar { + Button( + action: { + NSApp.keyWindow?.contentViewController?.tryToPerform( + #selector(NSSplitViewController.toggleSidebar(_:)), with: nil + ) + }, + label: { Image(systemName: "sidebar.left") } + ) + Spacer() + 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 { + DispatchQueue.main.async { + self.model.selectedPost = managedPost + } + } + }, label: { Image(systemName: "square.and.pencil") }) + } + #else + SidebarView() + #endif + #if os(macOS) PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn) + .toolbar { + ToolbarItemGroup(placement: .primaryAction) { + if let selectedPost = model.selectedPost { + ActivePostToolbarView(activePost: selectedPost) + .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 + }) + ) + }) + } + } + } + #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() @@ -51,6 +89,17 @@ struct ContentView: View { .environmentObject(model) } ) + .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 + }) + ) + }) #endif } } diff --git a/Shared/PostEditor/PostEditorStatusToolbarView.swift b/Shared/PostEditor/PostEditorStatusToolbarView.swift index 580e051..7d8cac4 100644 --- a/Shared/PostEditor/PostEditorStatusToolbarView.swift +++ b/Shared/PostEditor/PostEditorStatusToolbarView.swift @@ -11,16 +11,21 @@ struct PostEditorStatusToolbarView: View { PostStatusBadgeView(post: post) #else HStack { + HStack { + Text("⚠️ Newer copy on server. Replace local copy?") + .font(.callout) + .foregroundColor(.secondary) + Button(action: { + model.updateFromServer(post: post) + }, label: { + Image(systemName: "square.and.arrow.down") + }) + } + .padding(.horizontal) + .background(Color.primary.opacity(0.1)) + .clipShape(Capsule()) + .padding(.trailing) PostStatusBadgeView(post: post) - .padding(.trailing) - Text("⚠️ Newer copy on server. Replace local copy?") - .font(.callout) - .foregroundColor(.secondary) - Button(action: { - model.updateFromServer(post: post) - }, label: { - Image(systemName: "square.and.arrow.down") - }) } #endif } else if post.wasDeletedFromServer && post.status != PostStatus.local.rawValue { @@ -28,19 +33,24 @@ struct PostEditorStatusToolbarView: View { PostStatusBadgeView(post: post) #else HStack { + HStack { + Text("⚠️ Post deleted from server. Delete local copy?") + .font(.callout) + .foregroundColor(.secondary) + Button(action: { + model.selectedPost = nil + DispatchQueue.main.async { + model.posts.remove(post) + } + }, label: { + Image(systemName: "trash") + }) + } + .padding(.horizontal) + .background(Color.primary.opacity(0.1)) + .clipShape(Capsule()) + .padding(.trailing) PostStatusBadgeView(post: post) - .padding(.trailing) - Text("⚠️ Post deleted from server. Delete local copy?") - .font(.callout) - .foregroundColor(.secondary) - Button(action: { - model.selectedPost = nil - DispatchQueue.main.async { - model.posts.remove(post) - } - }, label: { - Image(systemName: "trash") - }) } #endif } else { diff --git a/Shared/PostList/PostListFilteredView.swift b/Shared/PostList/PostListFilteredView.swift index 6b37511..cc65779 100644 --- a/Shared/PostList/PostListFilteredView.swift +++ b/Shared/PostList/PostListFilteredView.swift @@ -86,6 +86,25 @@ struct PostListFilteredView: View { } }) } + .alert(isPresented: $model.isPresentingDeleteAlert) { + Alert( + title: Text("Delete Post?"), + message: Text("This action cannot be undone."), + primaryButton: .cancel() { + model.postToDelete = nil + }, + secondaryButton: .destructive(Text("Delete"), action: { + if let postToDelete = model.postToDelete { + model.selectedPost = nil + DispatchQueue.main.async { + model.editor.clearLastDraft() + model.posts.remove(postToDelete) + } + model.postToDelete = nil + } + }) + ) + } .onAppear(perform: { self.postCount = fetchRequest.wrappedValue.count }) @@ -103,7 +122,13 @@ struct PostListFilteredView: View { } func delete(_ post: WFAPost) { - model.posts.remove(post) + DispatchQueue.main.async { + if post == model.selectedPost { + model.selectedPost = nil + model.editor.clearLastDraft() + } + model.posts.remove(post) + } } } 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 @@ -3,8 +3,10 @@ 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() { diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index dddca79..ffc545b 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -21,7 +21,29 @@ struct PostListView: View { .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: { - createNewLocalDraft() + let managedPost = WFAPost(context: self.managedObjectContext) + 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.selectedCollection = nil + self.showAllPosts = false + self.model.selectedPost = managedPost + } }, label: { Image(systemName: "square.and.pencil") }) @@ -41,7 +63,10 @@ struct PostListView: View { ProgressView() } else { Button(action: { - reloadFromServer() + DispatchQueue.main.async { + model.fetchUserCollections() + model.fetchUserPosts() + } }, label: { Image(systemName: "arrow.clockwise") }) @@ -61,56 +86,8 @@ struct PostListView: View { ) ) .navigationSubtitle(postCount == 1 ? "\(postCount) post" : "\(postCount) posts") - .toolbar { - Button(action: { - createNewLocalDraft() - }, label: { - Image(systemName: "square.and.pencil") - }) - Button(action: { - reloadFromServer() - }, label: { - Image(systemName: "arrow.clockwise") - }) - .disabled(!model.account.isLoggedIn) - } #endif } - - private func reloadFromServer() { - DispatchQueue.main.async { - model.fetchUserCollections() - model.fetchUserPosts() - } - } - - private func createNewLocalDraft() { - let managedPost = WFAPost(context: self.managedObjectContext) - 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 - } - DispatchQueue.main.async { - self.selectedCollection = nil - self.showAllPosts = false - withAnimation { - self.model.selectedPost = managedPost - } - } - } } struct PostListView_Previews: PreviewProvider { diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift index ef3a31f..4aeb385 100644 --- a/Shared/WriteFreely_MultiPlatformApp.swift +++ b/Shared/WriteFreely_MultiPlatformApp.swift @@ -22,6 +22,27 @@ struct WriteFreely_MultiPlatformApp: App { .environment(\.managedObjectContext, LocalStorageManager.persistentContainer.viewContext) // .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info. } + .commands { + CommandGroup(replacing: .newItem, addition: { + Button("New Post") { + createNewLocalPost() + } + .keyboardShortcut("n", modifiers: [.command]) + }) + CommandGroup(after: .newItem) { + Button("Refresh Posts") { + DispatchQueue.main.async { + model.fetchUserCollections() + model.fetchUserPosts() + } + } + .disabled(!model.account.isLoggedIn) + .keyboardShortcut("r", modifiers: [.command]) + } + #if os(macOS) + SidebarCommands() + #endif + } #if os(macOS) Settings { @@ -48,6 +69,9 @@ struct WriteFreely_MultiPlatformApp: App { } private func createNewLocalPost() { + withAnimation { + self.model.selectedPost = nil + } let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) managedPost.createdDate = Date() managedPost.title = "" @@ -66,6 +90,8 @@ struct WriteFreely_MultiPlatformApp: App { managedPost.language = languageCode managedPost.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft } - self.model.selectedPost = managedPost + withAnimation { + self.model.selectedPost = managedPost + } } } diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 6829399..489cd38 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 17B996D92502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */; }; 17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */; }; 17B996DB2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */; }; + 17BC618A25715318003363CA /* ActivePostToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC617825715068003363CA /* ActivePostToolbarView.swift */; }; 17C42E622507D8E600072984 /* PostStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E612507D8E600072984 /* PostStatus.swift */; }; 17C42E632507D8E600072984 /* PostStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E612507D8E600072984 /* PostStatus.swift */; }; 17C42E652509237800072984 /* PostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E642509237800072984 /* PostListFilteredView.swift */; }; @@ -151,6 +152,7 @@ 17B5103A2515448D00E9631F /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; }; 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; + 17BC617825715068003363CA /* ActivePostToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePostToolbarView.swift; sourceTree = ""; }; 17C42E612507D8E600072984 /* PostStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStatus.swift; sourceTree = ""; }; 17C42E642509237800072984 /* PostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListFilteredView.swift; sourceTree = ""; }; 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+ExecuteAndMergeChanges.swift"; sourceTree = ""; }; @@ -316,6 +318,14 @@ path = PostEditor; sourceTree = ""; }; + 17BC617725715042003363CA /* Navigation */ = { + isa = PBXGroup; + children = ( + 17BC617825715068003363CA /* ActivePostToolbarView.swift */, + ); + path = Navigation; + sourceTree = ""; + }; 17D4F3722514EE4400517CE6 /* Resources */ = { isa = PBXGroup; children = ( @@ -393,6 +403,7 @@ children = ( 17DF329224C87D3500BCE2E3 /* Info.plist */, 17DF329324C87D3500BCE2E3 /* macOS.entitlements */, + 17BC617725715042003363CA /* Navigation */, 17A67CAC251A5D8D002F163D /* PostEditor */, 17A5388924DDA50500DEFF9A /* Settings */, 17B5103A2515448D00E9631F /* Credits.rtf */, @@ -754,6 +765,7 @@ 1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */, 1756DC0424FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */, 17B996DB2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, + 17BC618A25715318003363CA /* ActivePostToolbarView.swift in Sources */, 171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */, 17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */, 17DF32AB24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, diff --git a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist index 2723ebe..6cd8075 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_ orderHint - 0 + 1 WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_ orderHint - 1 + 0 diff --git a/macOS/Navigation/ActivePostToolbarView.swift b/macOS/Navigation/ActivePostToolbarView.swift new file mode 100644 index 0000000..68b9d93 --- /dev/null +++ b/macOS/Navigation/ActivePostToolbarView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct ActivePostToolbarView: View { + @EnvironmentObject var model: WriteFreelyModel + @ObservedObject var activePost: WFAPost + + var body: some View { + HStack(spacing: 16) { + PostEditorStatusToolbarView(post: activePost) + HStack(spacing: 4) { + Button(action: {}, label: { Image(systemName: "square.and.arrow.up") }) + .disabled(activePost.status == PostStatus.local.rawValue) + Button(action: { publishPost(activePost) }, label: { Image(systemName: "paperplane") }) + .disabled(activePost.body.isEmpty || activePost.status == PostStatus.published.rawValue) + } + } + } + + private func publishPost(_ post: WFAPost) { + DispatchQueue.main.async { + LocalStorageManager().saveContext() + model.publish(post: post) + } + } +} diff --git a/macOS/PostEditor/PostEditorView.swift b/macOS/PostEditor/PostEditorView.swift index a026563..4b7ab68 100644 --- a/macOS/PostEditor/PostEditorView.swift +++ b/macOS/PostEditor/PostEditorView.swift @@ -1,7 +1,6 @@ import SwiftUI struct PostEditorView: View { - private let bodyLineSpacing: CGFloat = 17 * 0.5 @EnvironmentObject var model: WriteFreelyModel @ObservedObject var post: WFAPost @@ -15,32 +14,34 @@ struct PostEditorView: View { ) .padding() .background(Color(NSColor.controlBackgroundColor)) - .toolbar { - ToolbarItem(placement: .status) { - PostEditorStatusToolbarView(post: post) + .onAppear(perform: { + if post.status != PostStatus.published.rawValue { + DispatchQueue.main.async { + self.model.editor.saveLastDraft(post) + } + } else { + self.model.editor.clearLastDraft() } - ToolbarItem(placement: .primaryAction) { - Button(action: { - if model.account.isLoggedIn { - publishPost() - } else { - let mainMenu = NSApplication.shared.mainMenu - let appMenuItem = mainMenu?.item(withTitle: "WriteFreely") - let prefsItem = appMenuItem?.submenu?.item(withTitle: "Preferences…") - NSApplication.shared.sendAction(prefsItem!.action!, to: prefsItem?.target, from: nil) - } - }, label: { - Image(systemName: "paperplane") - }) - .disabled(post.status == PostStatus.published.rawValue || post.body.count == 0) - } - } + }) .onChange(of: post.hasNewerRemoteCopy, perform: { _ in if !post.hasNewerRemoteCopy { self.updatingFromServer = true } }) + .onChange(of: post.status, perform: { value in + if value != PostStatus.published.rawValue { + self.model.editor.saveLastDraft(post) + } else { + self.model.editor.clearLastDraft() + } + DispatchQueue.main.async { + LocalStorageManager().saveContext() + } + }) .onDisappear(perform: { + DispatchQueue.main.async { + model.editor.clearLastDraft() + } if post.title.count == 0 && post.body.count == 0 && post.status == PostStatus.local.rawValue @@ -56,13 +57,6 @@ struct PostEditorView: View { } }) } - - private func publishPost() { - DispatchQueue.main.async { - LocalStorageManager().saveContext() - model.publish(post: post) - } - } } struct PostEditorView_EmptyPostPreviews: PreviewProvider {