diff --git a/Shared/Account/AccountLoginView.swift b/Shared/Account/AccountLoginView.swift index b77d915..de0390b 100644 --- a/Shared/Account/AccountLoginView.swift +++ b/Shared/Account/AccountLoginView.swift @@ -1,111 +1,109 @@ import SwiftUI struct AccountLoginView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling @State private var alertMessage: String = "" @State private var username: String = "" @State private var password: String = "" @State private var server: String = "" var body: some View { VStack { Text("Log in to publish and share your posts.") .font(.caption) .foregroundColor(.secondary) HStack { Image(systemName: "person.circle") .foregroundColor(.gray) #if os(iOS) TextField("Username", text: $username) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(RoundedBorderTextFieldStyle()) #else TextField("Username", text: $username) #endif } HStack { Image(systemName: "lock.circle") .foregroundColor(.gray) #if os(iOS) SecureField("Password", text: $password) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(RoundedBorderTextFieldStyle()) #else SecureField("Password", text: $password) #endif } HStack { Image(systemName: "link.circle") .foregroundColor(.gray) #if os(iOS) TextField("Server URL", text: $server) .keyboardType(.URL) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(RoundedBorderTextFieldStyle()) #else TextField("Server URL", text: $server) #endif } Spacer() if model.isLoggingIn { ProgressView("Logging in...") .padding() } else { Button(action: { #if os(iOS) hideKeyboard() #endif // If the server string is not prefixed with a scheme, prepend "https://" to it. if !(server.hasPrefix("https://") || server.hasPrefix("http://")) { server = "https://\(server)" } // We only need the protocol and host from the URL, so drop anything else. let url = URLComponents(string: server) if let validURL = url { let scheme = validURL.scheme let host = validURL.host var hostURL = URLComponents() hostURL.scheme = scheme hostURL.host = host server = hostURL.string ?? server model.login( to: URL(string: server)!, as: username, password: password ) } else { self.errorHandling.handle(error: AccountError.invalidServerURL) } }, label: { Text("Log In") }) .disabled( model.account.isLoggedIn || (username.isEmpty || password.isEmpty || server.isEmpty) ) .padding() } } .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } } } struct AccountLoginView_Previews: PreviewProvider { static var previews: some View { AccountLoginView() .environmentObject(WriteFreelyModel()) } } diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift index e373aed..0b54f0e 100644 --- a/Shared/Account/AccountView.swift +++ b/Shared/Account/AccountView.swift @@ -1,42 +1,40 @@ import SwiftUI struct AccountView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling var body: some View { if model.account.isLoggedIn { HStack { Spacer() AccountLogoutView() .withErrorHandling() Spacer() } .padding() } else { AccountLoginView() .withErrorHandling() .padding(.top) } EmptyView() .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } } } struct AccountLogin_Previews: PreviewProvider { static var previews: some View { AccountView() .environmentObject(WriteFreelyModel()) } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 77c6684..f67cda9 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -1,89 +1,87 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling var body: some View { NavigationView { #if os(macOS) CollectionListView() .withErrorHandling() .toolbar { Button( action: { NSApp.keyWindow?.contentViewController?.tryToPerform( #selector(NSSplitViewController.toggleSidebar(_:)), with: nil ) }, label: { Image(systemName: "sidebar.left") } ) .help("Toggle the sidebar's visibility.") Spacer() Button(action: { withAnimation { // Un-set the currently selected post self.model.selectedPost = nil } // Create the new-post managed object let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font) withAnimation { DispatchQueue.main.async { // Load the new post in the editor self.model.selectedPost = managedPost } } }, label: { Image(systemName: "square.and.pencil") }) .help("Create a new local draft.") } .frame(width: 200) #else CollectionListView() .withErrorHandling() #endif #if os(macOS) ZStack { PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) .withErrorHandling() .frame(width: 300) if model.isProcessingRequest { ZStack { Color(NSColor.controlBackgroundColor).opacity(0.75) ProgressView() } } } #else PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) .withErrorHandling() #endif NoSelectedPostView(isConnected: $model.hasNetworkConnection) } .environmentObject(model) .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() return ContentView() .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index fc41637..2c3dab7 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -1,71 +1,69 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling @FetchRequest(sortDescriptors: []) var collections: FetchedResults @State var selectedCollection: WFACollection? var body: some View { List(selection: $selectedCollection) { if model.account.isLoggedIn { NavigationLink("All Posts", destination: PostListView(selectedCollection: nil, showAllPosts: true)) NavigationLink( model.account.server == "https://write.as" ? "Anonymous" : "Drafts", destination: PostListView(selectedCollection: nil, showAllPosts: false) ) Section(header: Text("Your Blogs")) { ForEach(collections, id: \.self) { collection in NavigationLink(destination: PostListView(selectedCollection: collection, showAllPosts: false), tag: collection, selection: $selectedCollection, label: { Text("\(collection.title)") }) } } } else { NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) { Text("Drafts") } } } .navigationTitle( model.account.isLoggedIn ? "\(URL(string: model.account.server)?.host ?? "WriteFreely")" : "WriteFreely" ) .listStyle(SidebarListStyle()) .onChange(of: model.selectedCollection) { collection in model.selectedPost = nil if collection != model.editor.fetchSelectedCollectionFromAppStorage() { self.model.editor.selectedCollectionURL = collection?.objectID.uriRepresentation() } } .onChange(of: model.showAllPosts) { value in model.selectedPost = nil if value != model.editor.showAllPostsFlag { self.model.editor.showAllPostsFlag = model.showAllPosts } } .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } } } struct CollectionListView_LoggedOutPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() return CollectionListView() .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 0146f39..89fe9fa 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -1,201 +1,197 @@ import SwiftUI import Combine struct PostListView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling @Environment(\.managedObjectContext) var managedObjectContext @State private var postCount: Int = 0 @State private var filteredListViewId: Int = 0 var selectedCollection: WFACollection? var showAllPosts: Bool #if os(iOS) private var frameHeight: CGFloat { var height: CGFloat = 50 let bottom = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 height += bottom return height } #endif var body: some View { #if os(iOS) ZStack(alignment: .bottom) { PostListFilteredView( collection: selectedCollection, showAllPosts: showAllPosts, postCount: $postCount ) .id(self.filteredListViewId) .navigationTitle( showAllPosts ? "All Posts" : selectedCollection?.title ?? ( model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ) ) .toolbar { ToolbarItem(placement: .primaryAction) { ZStack { // We have to add a Spacer as a sibling view to the Button in some kind of Stack so that any // a11y modifiers are applied as expected: bug report filed as FB8956392. if #unavailable(iOS 16) { Spacer() } Button(action: { let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font) withAnimation { self.model.showAllPosts = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.model.selectedPost = managedPost } } }, label: { ZStack { Image("does.not.exist") .accessibilityHidden(true) Image(systemName: "square.and.pencil") .accessibilityHidden(true) .imageScale(.large) // These modifiers compensate for the resizing .padding(.vertical, 12) // done to the Image (and the button tap target) .padding(.leading, 12) // by the SwiftUI layout system from adding a .padding(.trailing, 8) // Spacer in this ZStack (FB8956392). } .frame(maxWidth: .infinity, maxHeight: .infinity) }) .accessibilityLabel(Text("Compose")) .accessibilityHint(Text("Compose a new local draft")) } } } VStack { HStack(spacing: 0) { Button(action: { model.isPresentingSettingsView = true }, label: { Image(systemName: "gear") .padding(.vertical, 4) .padding(.horizontal, 8) }) .accessibilityLabel(Text("Settings")) .accessibilityHint(Text("Open the Settings sheet")) .sheet( isPresented: $model.isPresentingSettingsView, onDismiss: { model.isPresentingSettingsView = false }, content: { SettingsView() .environmentObject(model) } ) Spacer() Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts") .foregroundColor(.secondary) Spacer() if model.isProcessingRequest { ProgressView() .padding(.vertical, 4) .padding(.horizontal, 8) } else { if model.hasNetworkConnection { Button(action: { DispatchQueue.main.async { model.fetchUserCollections() model.fetchUserPosts() } }, label: { Image(systemName: "arrow.clockwise") .padding(.vertical, 4) .padding(.horizontal, 8) }) .accessibilityLabel(Text("Refresh Posts")) .accessibilityHint(Text("Fetch changes from the server")) .disabled(!model.account.isLoggedIn) } else { Image(systemName: "wifi.exclamationmark") .padding(.vertical, 4) .padding(.horizontal, 8) .foregroundColor(.secondary) } } } .padding(.top, 8) .padding(.horizontal, 8) Spacer() } .frame(height: frameHeight) .background(Color(UIColor.systemGray5)) .overlay(Divider(), alignment: .top) .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in // We use this to invalidate and refresh the view, so that new posts created outside of the app (e.g., // in the action extension) show up. withAnimation { self.filteredListViewId += 1 } } } .ignoresSafeArea(.all, edges: .bottom) .onAppear { model.selectedCollection = selectedCollection model.showAllPosts = showAllPosts } .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } #else PostListFilteredView( collection: selectedCollection, showAllPosts: showAllPosts, postCount: $postCount ) .toolbar { ToolbarItemGroup(placement: .primaryAction) { if model.selectedPost != nil { ActivePostToolbarView(activePost: model.selectedPost!) } } } .navigationTitle( showAllPosts ? "All Posts" : selectedCollection?.title ?? ( model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ) ) .onAppear { model.selectedCollection = selectedCollection model.showAllPosts = showAllPosts } .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } #endif } } struct PostListView_Previews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() return PostListView(showAllPosts: true) .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift index 97b0927..070b421 100644 --- a/iOS/PostEditor/PostEditorView.swift +++ b/iOS/PostEditor/PostEditorView.swift @@ -1,288 +1,286 @@ import SwiftUI struct PostEditorView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.managedObjectContext) var moc @Environment(\.presentationMode) var presentationMode @ObservedObject var post: WFAPost @State private var updatingTitleFromServer: Bool = false @State private var updatingBodyFromServer: Bool = false @State private var selectedCollection: WFACollection? @FetchRequest( entity: WFACollection.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)] ) var collections: FetchedResults var body: some View { VStack { if post.hasNewerRemoteCopy { RemoteChangePromptView( remoteChangeType: .remoteCopyUpdated, buttonHandler: { model.updateFromServer(post: post) } ) } else if post.wasDeletedFromServer { RemoteChangePromptView( remoteChangeType: .remoteCopyDeleted, buttonHandler: { self.presentationMode.wrappedValue.dismiss() DispatchQueue.main.async { model.posts.remove(post) } } ) } PostTextEditingView( post: _post, updatingTitleFromServer: $updatingTitleFromServer, updatingBodyFromServer: $updatingBodyFromServer ) .withErrorHandling() } .navigationBarTitleDisplayMode(.inline) .padding() .toolbar { ToolbarItem(placement: .principal) { PostEditorStatusToolbarView(post: post) } ToolbarItem(placement: .primaryAction) { if !model.hasNetworkConnection { Image(systemName: "wifi.exclamationmark") .foregroundColor(.secondary) } else if model.isProcessingRequest { ProgressView() } else { Menu(content: { if post.status == PostStatus.local.rawValue { Menu(content: { Label("Publish to…", systemImage: "paperplane") Button(action: { if model.account.isLoggedIn { post.collectionAlias = nil publishPost() } else { self.model.isPresentingSettingsView = true } }, label: { Text(" \(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")") }) ForEach(collections) { collection in Button(action: { if model.account.isLoggedIn { post.collectionAlias = collection.alias publishPost() } else { self.model.isPresentingSettingsView = true } }, label: { Text(" \(collection.title)") }) } }, label: { Label("Publish…", systemImage: "paperplane") }) .accessibilityHint(Text("Choose the blog you want to publish this post to")) .disabled(post.body.count == 0) } else if post.status == PostStatus.edited.rawValue { Button(action: { if model.account.isLoggedIn { publishPost() } else { self.model.isPresentingSettingsView = true } }, label: { Label("Publish", systemImage: "paperplane") }) .disabled(post.status == PostStatus.published.rawValue || post.body.count == 0) Button(action: { model.updateFromServer(post: post) }, label: { Label("Revert", systemImage: "clock.arrow.circlepath") }) .accessibilityHint(Text("Replace the edited post with the published version from the server")) .disabled(post.status != PostStatus.edited.rawValue) } Button(action: { sharePost() }, label: { Label("Share", systemImage: "square.and.arrow.up") }) .accessibilityHint(Text("Open the system share sheet to share a link to this post")) .disabled(post.postId == nil) if model.account.isLoggedIn && post.status != PostStatus.local.rawValue { Section(header: Text("Move To Collection")) { Label("Move to:", systemImage: "arrowshape.zigzag.right") Picker(selection: $selectedCollection, label: Text("Move to…")) { Text( " \(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")" ).tag(nil as WFACollection?) ForEach(collections) { collection in Text(" \(collection.title)").tag(collection as WFACollection?) } } } } }, label: { ZStack { Image("does.not.exist") .accessibilityHidden(true) Image(systemName: "ellipsis.circle") .imageScale(.large) .accessibilityHidden(true) } }) .accessibilityLabel(Text("Menu")) .accessibilityHint(Text("Opens a context menu to publish, share, or move the post")) .onTapGesture { hideKeyboard() } .disabled(post.body.count == 0) } } } .onChange(of: post.hasNewerRemoteCopy, perform: { _ in if !post.hasNewerRemoteCopy { updatingTitleFromServer = true updatingBodyFromServer = true } }) .onChange(of: selectedCollection, perform: { [selectedCollection] newCollection in if post.collectionAlias == newCollection?.alias { return } else { post.collectionAlias = newCollection?.alias model.move(post: post, from: selectedCollection, to: newCollection) } }) .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.standard.saveContext() } }) .onAppear(perform: { self.selectedCollection = collections.first { $0.alias == post.collectionAlias } model.editor.setInitialValues(for: post) if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { self.model.editor.saveLastDraft(post) } } else { self.model.editor.clearLastDraft() } }) .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } .onDisappear(perform: { self.model.editor.clearLastDraft() if post.title.count == 0 && post.body.count == 0 && post.status == PostStatus.local.rawValue && post.updatedDate == nil && post.postId == nil { DispatchQueue.main.async { model.posts.remove(post) } } else if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { LocalStorageManager.standard.saveContext() } } }) } private func publishPost() { DispatchQueue.main.async { LocalStorageManager.standard.saveContext() model.publish(post: post) } model.editor.setInitialValues(for: post) self.hideKeyboard() } private func sharePost() { // If the post doesn't have a post ID, it isn't published, and therefore can't be shared, so return early. guard let postId = post.postId else { return } var urlString: String if let postSlug = post.slug, let postCollectionAlias = post.collectionAlias { // This post is in a collection, so share the URL as baseURL/postSlug. let urls = collections.filter { $0.alias == postCollectionAlias } let baseURL = urls.first?.url ?? "\(model.account.server)/\(postCollectionAlias)/" urlString = "\(baseURL)\(postSlug)" } else { // This is a draft post, so share the URL as server/postID urlString = "\(model.account.server)/\(postId)" } guard let data = URL(string: urlString) else { return } let activityView = UIActivityViewController(activityItems: [data], applicationActivities: nil) UIApplication.shared.windows.first?.rootViewController?.present(activityView, animated: true, completion: nil) if UIDevice.current.userInterfaceIdiom == .pad { activityView.popoverPresentationController?.permittedArrowDirections = .up activityView.popoverPresentationController?.sourceView = UIApplication.shared.windows.first activityView.popoverPresentationController?.sourceRect = CGRect( x: UIScreen.main.bounds.width, y: -125, width: 200, height: 200 ) } } } struct PostEditorView_EmptyPostPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.createdDate = Date() testPost.appearance = "norm" let model = WriteFreelyModel() return PostEditorView(post: testPost) .environment(\.managedObjectContext, context) .environmentObject(model) } } struct PostEditorView_ExistingPostPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." testPost.createdDate = Date() testPost.appearance = "code" testPost.hasNewerRemoteCopy = true let model = WriteFreelyModel() return PostEditorView(post: testPost) .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/macOS/PostEditor/PostEditorView.swift b/macOS/PostEditor/PostEditorView.swift index ff82a93..fc6fb62 100644 --- a/macOS/PostEditor/PostEditorView.swift +++ b/macOS/PostEditor/PostEditorView.swift @@ -1,113 +1,111 @@ import SwiftUI struct PostEditorView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling @ObservedObject var post: WFAPost @State private var isHovering: Bool = false @State private var updatingFromServer: Bool = false var body: some View { VStack { if !model.hasNetworkConnection { Label("You are not connected to the internet", systemImage: "wifi.exclamationmark") .font(.caption) .foregroundColor(.secondary) .padding(.top, 8) } PostTextEditingView( post: post, updatingFromServer: $updatingFromServer ) .background(Color(NSColor.controlBackgroundColor)) .onAppear(perform: { model.editor.setInitialValues(for: post) if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { self.model.editor.saveLastDraft(post) } } else { self.model.editor.clearLastDraft() } }) .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.standard.saveContext() } }) .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } .onDisappear(perform: { DispatchQueue.main.async { model.editor.clearLastDraft() } if post.title.count == 0 && post.body.count == 0 && post.status == PostStatus.local.rawValue && post.updatedDate == nil && post.postId == nil { DispatchQueue.main.async { model.posts.remove(post) } } else if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { LocalStorageManager.standard.saveContext() } } }) } } } struct PostEditorView_EmptyPostPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.createdDate = Date() testPost.appearance = "norm" let model = WriteFreelyModel() return PostEditorView(post: testPost) .environment(\.managedObjectContext, context) .environmentObject(model) } } struct PostEditorView_ExistingPostPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." testPost.createdDate = Date() testPost.appearance = "code" let model = WriteFreelyModel() return PostEditorView(post: testPost) .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/macOS/Settings/MacAccountView.swift b/macOS/Settings/MacAccountView.swift index 44fa8d9..9939e99 100644 --- a/macOS/Settings/MacAccountView.swift +++ b/macOS/Settings/MacAccountView.swift @@ -1,31 +1,29 @@ import SwiftUI struct MacAccountView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling var body: some View { Form { AccountView() } .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } } } struct MacAccountView_Previews: PreviewProvider { static var previews: some View { MacAccountView() .environmentObject(WriteFreelyModel()) } } diff --git a/macOS/Settings/MacPreferencesView.swift b/macOS/Settings/MacPreferencesView.swift index ad48e73..feb91e5 100644 --- a/macOS/Settings/MacPreferencesView.swift +++ b/macOS/Settings/MacPreferencesView.swift @@ -1,33 +1,31 @@ import SwiftUI struct MacPreferencesView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling @ObservedObject var preferences: PreferencesModel var body: some View { VStack { PreferencesView(preferences: preferences) Spacer() } .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } } } struct MacPreferencesView_Previews: PreviewProvider { static var previews: some View { MacPreferencesView(preferences: PreferencesModel()) } } diff --git a/macOS/Settings/MacUpdatesView.swift b/macOS/Settings/MacUpdatesView.swift index 1f13f52..ba9f6a3 100644 --- a/macOS/Settings/MacUpdatesView.swift +++ b/macOS/Settings/MacUpdatesView.swift @@ -1,106 +1,104 @@ import SwiftUI import Sparkle struct MacUpdatesView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling @ObservedObject var updaterViewModel: MacUpdatesViewModel @AppStorage(WFDefaults.automaticallyChecksForUpdates, store: UserDefaults.shared) var automaticallyChecksForUpdates: Bool = false @AppStorage(WFDefaults.subscribeToBetaUpdates, store: UserDefaults.shared) var subscribeToBetaUpdates: Bool = false @State private var lastUpdateCheck: Date? private let betaWarningString = """ To get brand new features before each official release, choose "Test versions." Note that test versions may have bugs \ that can cause crashes and data loss. """ static let lastUpdateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .short formatter.doesRelativeDateFormatting = true return formatter }() var body: some View { VStack(spacing: 24) { Toggle(isOn: $automaticallyChecksForUpdates, label: { Text("Check for updates automatically") }) VStack { Button(action: { updaterViewModel.checkForUpdates() // There's a delay between requesting an update, and the timestamp for that update request being // written to user defaults; we therefore delay updating the "Last checked" UI for one second. DispatchQueue.main.asyncAfter(deadline: .now() + 1) { lastUpdateCheck = updaterViewModel.getLastUpdateCheckDate() } }, label: { Text("Check For Updates") }) HStack { Text("Last checked:") .font(.caption) if let lastUpdateCheck = lastUpdateCheck { Text(lastUpdateCheck, formatter: Self.lastUpdateFormatter) .font(.caption) } else { Text("Never") .font(.caption) } } } VStack(spacing: 16) { HStack(alignment: .top) { Text("Download:") Picker(selection: $subscribeToBetaUpdates, label: Text("Download:"), content: { Text("Release versions").tag(false) Text("Test versions").tag(true) }) .pickerStyle(RadioGroupPickerStyle()) .labelsHidden() } Text(betaWarningString) .frame(width: 350) .foregroundColor(.secondary) } } .padding() .onAppear { lastUpdateCheck = updaterViewModel.getLastUpdateCheckDate() } .onChange(of: automaticallyChecksForUpdates) { value in updaterViewModel.automaticallyCheckForUpdates = value } .onChange(of: subscribeToBetaUpdates) { _ in updaterViewModel.toggleAllowedChannels() } .onChange(of: model.hasError) { value in if value { - if model.hasNetworkConnection { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError()) - } + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } } } } struct MacUpdatesView_Previews: PreviewProvider { static var previews: some View { MacUpdatesView(updaterViewModel: MacUpdatesViewModel()) } }